ledger_sync 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_todo.yml +25 -0
- data/.travis.yml +1 -1
- data/Dockerfile +8 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +137 -8
- data/README.md +184 -9
- data/Rakefile +3 -3
- data/bin/console +3 -3
- data/docker-compose.yml +4 -0
- data/ledger_sync.gemspec +32 -11
- data/lib/ledger_sync.rb +115 -3
- data/lib/ledger_sync/adaptor_configuration.rb +55 -0
- data/lib/ledger_sync/adaptor_configuration_store.rb +52 -0
- data/lib/ledger_sync/adaptors/adaptor.rb +66 -0
- data/lib/ledger_sync/adaptors/contract.rb +16 -0
- data/lib/ledger_sync/adaptors/operation.rb +213 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/adaptor.rb +161 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/config.rb +7 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/customer/operations/create.rb +44 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/customer/operations/find.rb +35 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/customer/operations/update.rb +53 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/customer/operations/upsert.rb +42 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/customer/searcher.rb +63 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/invoice/operations/create.rb +63 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/invoice/operations/find.rb +36 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/invoice/operations/update.rb +67 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/invoice/operations/upsert.rb +44 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/payment/operations/create.rb +64 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/payment/operations/find.rb +35 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/payment/operations/update.rb +64 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/payment/operations/upsert.rb +53 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/product/operations/create.rb +46 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/product/operations/find.rb +34 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/product/operations/update.rb +50 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/product/operations/upsert.rb +43 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/util/adaptor_error_parser.rb +102 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/util/error_matcher.rb +54 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/util/error_parser.rb +27 -0
- data/lib/ledger_sync/adaptors/quickbooks_online/util/operation_error_parser.rb +96 -0
- data/lib/ledger_sync/adaptors/searcher.rb +64 -0
- data/lib/ledger_sync/adaptors/test/adaptor.rb +47 -0
- data/lib/ledger_sync/adaptors/test/config.rb +7 -0
- data/lib/ledger_sync/adaptors/test/customer/operations/create.rb +49 -0
- data/lib/ledger_sync/adaptors/test/customer/operations/find.rb +35 -0
- data/lib/ledger_sync/adaptors/test/customer/operations/invalid.rb +20 -0
- data/lib/ledger_sync/adaptors/test/customer/operations/update.rb +46 -0
- data/lib/ledger_sync/adaptors/test/customer/operations/upsert.rb +42 -0
- data/lib/ledger_sync/adaptors/test/customer/operations/valid.rb +26 -0
- data/lib/ledger_sync/adaptors/test/customer/searcher.rb +40 -0
- data/lib/ledger_sync/adaptors/test/error/adaptor_error/operations/throttle_error.rb +28 -0
- data/lib/ledger_sync/adaptors/test/payment/operations/create.rb +56 -0
- data/lib/ledger_sync/adaptors/test/payment/operations/find.rb +35 -0
- data/lib/ledger_sync/adaptors/test/payment/operations/update.rb +62 -0
- data/lib/ledger_sync/adaptors/test/payment/operations/upsert.rb +53 -0
- data/lib/ledger_sync/concerns/validatable.rb +19 -0
- data/lib/ledger_sync/core_ext/resonad.rb +16 -0
- data/lib/ledger_sync/error.rb +10 -0
- data/lib/ledger_sync/error/adaptor_errors.rb +47 -0
- data/lib/ledger_sync/error/operation_errors.rb +41 -0
- data/lib/ledger_sync/error/resource_errors.rb +20 -0
- data/lib/ledger_sync/resource.rb +92 -0
- data/lib/ledger_sync/resources/customer.rb +8 -0
- data/lib/ledger_sync/resources/invoice.rb +12 -0
- data/lib/ledger_sync/resources/payment.rb +11 -0
- data/lib/ledger_sync/resources/product.rb +6 -0
- data/lib/ledger_sync/resources/vendor.rb +6 -0
- data/lib/ledger_sync/result.rb +140 -0
- data/lib/ledger_sync/sync.rb +107 -0
- data/lib/ledger_sync/util/coordinator.rb +72 -0
- data/lib/ledger_sync/util/debug.rb +16 -0
- data/lib/ledger_sync/util/hash_helpers.rb +13 -0
- data/lib/ledger_sync/util/performer.rb +29 -0
- data/lib/ledger_sync/util/resources_builder.rb +68 -0
- data/lib/ledger_sync/util/string_helpers.rb +37 -0
- data/lib/ledger_sync/util/validator.rb +49 -0
- data/lib/ledger_sync/version.rb +1 -1
- data/release.sh +8 -0
- metadata +334 -11
- data/.rspec +0 -3
@@ -0,0 +1,53 @@
|
|
1
|
+
module LedgerSync
|
2
|
+
module Adaptors
|
3
|
+
module Test
|
4
|
+
module Payment
|
5
|
+
module Operations
|
6
|
+
class Upsert < Operation::Upsert
|
7
|
+
class Contract < LedgerSync::Adaptors::Contract
|
8
|
+
schema do
|
9
|
+
required(:ledger_id).maybe(:string)
|
10
|
+
required(:amount).value(:integer)
|
11
|
+
required(:currency).value(:string)
|
12
|
+
required(:customer).value(Types::Reference)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def build
|
19
|
+
op = if qbo_payment?
|
20
|
+
Update.new(adaptor: adaptor, resource: resource)
|
21
|
+
else
|
22
|
+
Create.new(adaptor: adaptor, resource: resource)
|
23
|
+
end
|
24
|
+
|
25
|
+
build_customer_operation
|
26
|
+
add_root_operation(op)
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_customer_operation
|
30
|
+
customer = Customer::Operations::Upsert.new(
|
31
|
+
adaptor: adaptor,
|
32
|
+
resource: resource.customer
|
33
|
+
)
|
34
|
+
|
35
|
+
add_before_operation(customer)
|
36
|
+
end
|
37
|
+
|
38
|
+
def find_result
|
39
|
+
@find_result ||= Find.new(
|
40
|
+
adaptor: adaptor,
|
41
|
+
resource: resource
|
42
|
+
).perform
|
43
|
+
end
|
44
|
+
|
45
|
+
def qbo_payment?
|
46
|
+
find_result.success?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module LedgerSync
|
2
|
+
module Validatable
|
3
|
+
def valid?
|
4
|
+
validate.success?
|
5
|
+
end
|
6
|
+
|
7
|
+
def validate
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate_or_fail
|
12
|
+
if valid?
|
13
|
+
Resonad.Success(self)
|
14
|
+
else
|
15
|
+
Resonad.Failure
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ledger_sync/util/debug'
|
4
|
+
require 'simply_serializable'
|
5
|
+
|
6
|
+
class Resonad
|
7
|
+
include SimplySerializable::Mixin
|
8
|
+
|
9
|
+
class Success < Resonad
|
10
|
+
serialize only: %i[value]
|
11
|
+
end
|
12
|
+
|
13
|
+
class Failure < Resonad
|
14
|
+
serialize only: %i[error]
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LedgerSync
|
4
|
+
class Error
|
5
|
+
class AdaptorError < Error
|
6
|
+
attr_reader :adaptor
|
7
|
+
|
8
|
+
def initialize(adaptor:, message:)
|
9
|
+
@adaptor = adaptor
|
10
|
+
super(message: message)
|
11
|
+
end
|
12
|
+
|
13
|
+
class MissingAdaptorError < self
|
14
|
+
def initialize(message:)
|
15
|
+
super(message: message, adaptor: nil)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class AdaptorValidationError < self
|
20
|
+
attr_reader :attribute, :validation
|
21
|
+
|
22
|
+
def initialize(message:, adaptor:, attribute:, validation:)
|
23
|
+
@attribute = attribute
|
24
|
+
@validation = validation
|
25
|
+
super(message: message, adaptor: adaptor)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class ThrottleError < self
|
30
|
+
attr_reader :rate_limiting_wait_in_seconds
|
31
|
+
|
32
|
+
def initialize(adaptor:, message: nil)
|
33
|
+
message ||= 'Your request has been throttled.'
|
34
|
+
@rate_limiting_wait_in_seconds = LedgerSync.adaptors.config_from_klass(
|
35
|
+
klass: adaptor.class
|
36
|
+
).rate_limiting_wait_in_seconds
|
37
|
+
|
38
|
+
super(adaptor: adaptor, message: message)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class AuthenticationError < self; end
|
43
|
+
class AuthorizationError < self; end
|
44
|
+
class ConfigurationError < self; end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LedgerSync
|
4
|
+
class Error
|
5
|
+
class OperationError < Error
|
6
|
+
attr_reader :operation
|
7
|
+
|
8
|
+
def initialize(message:, operation:)
|
9
|
+
@operation = operation
|
10
|
+
super(message: message)
|
11
|
+
end
|
12
|
+
|
13
|
+
class DuplicateLedgerResourceError < self; end
|
14
|
+
class NotFoundError < self; end
|
15
|
+
class LedgerValidationError < self; end
|
16
|
+
|
17
|
+
class PerformedOperationError < self
|
18
|
+
def initialize(message: nil, operation:)
|
19
|
+
message ||= 'Operation has already been performed. Please check the result.'
|
20
|
+
|
21
|
+
super(message: message, operation: operation)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class ValidationError < self
|
26
|
+
attr_reader :attribute,
|
27
|
+
:validation
|
28
|
+
|
29
|
+
def initialize(message:, attribute:, operation:, validation:)
|
30
|
+
@attribute = attribute
|
31
|
+
@validation = validation
|
32
|
+
|
33
|
+
super(
|
34
|
+
message: message,
|
35
|
+
operation: operation
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module LedgerSync
|
2
|
+
class ResourceError < Error
|
3
|
+
attr_reader :resource
|
4
|
+
|
5
|
+
def initialize(message:, resource:)
|
6
|
+
@resource = resource
|
7
|
+
super(message: message)
|
8
|
+
end
|
9
|
+
|
10
|
+
class MissingResourceError < self
|
11
|
+
attr_reader :resource_type, :resource_external_id
|
12
|
+
|
13
|
+
def initialize(message:, resource_type:, resource_external_id:)
|
14
|
+
@resource_type = resource_type
|
15
|
+
@resource_external_id = resource_external_id
|
16
|
+
super(message: message, resource: nil)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Template class for named resources such as
|
4
|
+
# LedgerSync::Invoice, LedgerSync::Contact, etc.
|
5
|
+
module LedgerSync
|
6
|
+
class Resource
|
7
|
+
include SimplySerializable::Mixin
|
8
|
+
include Validatable
|
9
|
+
include Fingerprintable::Mixin
|
10
|
+
|
11
|
+
# serialize only: %i[
|
12
|
+
# attributes
|
13
|
+
# external_id
|
14
|
+
# ledger_id
|
15
|
+
# sync_token
|
16
|
+
# ]
|
17
|
+
|
18
|
+
attr_accessor :external_id, :ledger_id, :sync_token
|
19
|
+
|
20
|
+
def initialize(external_id: nil, ledger_id: nil, sync_token: nil, **data)
|
21
|
+
@external_id = external_id.to_s.to_sym
|
22
|
+
@ledger_id = ledger_id
|
23
|
+
@sync_token = sync_token
|
24
|
+
|
25
|
+
data.each do |attr_key, val|
|
26
|
+
if (self.class.references || {}).key?(attr_key)
|
27
|
+
raise "#{val} must be of type #{self.class.references[attr_key]}" if self.class.references[attr_key] != val.class
|
28
|
+
end
|
29
|
+
|
30
|
+
raise "#{attr_key} is not an attribute of #{self.class}" unless self.class.attributes.include?(attr_key)
|
31
|
+
end
|
32
|
+
|
33
|
+
self.class.attributes.each do |attribute|
|
34
|
+
instance_variable_set("@#{attribute}", data.dig(attribute))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def attributes
|
39
|
+
self.class.attributes
|
40
|
+
end
|
41
|
+
|
42
|
+
def references
|
43
|
+
self.class.references
|
44
|
+
end
|
45
|
+
|
46
|
+
def serialize_attributes
|
47
|
+
Hash[self.class.attributes.map { |a| [a, send(a)] }]
|
48
|
+
end
|
49
|
+
|
50
|
+
# def serializable_type
|
51
|
+
# self.class.resource_type
|
52
|
+
# end
|
53
|
+
|
54
|
+
def self.attribute(name)
|
55
|
+
attributes << name.to_sym
|
56
|
+
class_eval { attr_accessor name }
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.attributes
|
60
|
+
@attributes ||= []
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.klass_from_resource_type(obj)
|
64
|
+
LedgerSync.const_get(LedgerSync::Util::StringHelpers.camelcase(obj))
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.reference(name, type)
|
68
|
+
attribute(name)
|
69
|
+
references[name.to_sym] = type
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.references
|
73
|
+
@references ||= {}
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.reference_klass(name)
|
77
|
+
references[name.to_sym]
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.reference_resource_type(name)
|
81
|
+
reference_klass(name).resource_type
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.resource_type
|
85
|
+
@resource_type ||= LedgerSync::Util::StringHelpers.underscore(name.split('::').last).to_sym
|
86
|
+
end
|
87
|
+
|
88
|
+
def ==(other)
|
89
|
+
other.fingerprint == fingerprint
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module LedgerSync
|
2
|
+
module ResultBase
|
3
|
+
module HelperMethods
|
4
|
+
def Success(value = nil, *args)
|
5
|
+
self::Success.new(value, *args)
|
6
|
+
end
|
7
|
+
|
8
|
+
def Failure(error = nil, *args)
|
9
|
+
self::Failure.new(error, *args)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.included(base)
|
14
|
+
base.const_set('Success', Class.new(Resonad::Success))
|
15
|
+
base::Success.include base::ResultTypeBase if base.const_defined?('ResultTypeBase')
|
16
|
+
|
17
|
+
base.const_set('Failure', Class.new(Resonad::Failure))
|
18
|
+
base::Failure.include base::ResultTypeBase if base.const_defined?('ResultTypeBase')
|
19
|
+
|
20
|
+
base.extend HelperMethods
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Result
|
25
|
+
include ResultBase
|
26
|
+
end
|
27
|
+
|
28
|
+
class OperationResult
|
29
|
+
module ResultTypeBase
|
30
|
+
attr_reader :operation, :response
|
31
|
+
|
32
|
+
def self.included(base)
|
33
|
+
base.class_eval do
|
34
|
+
serialize only: %i[operation response]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(*args, operation:, response:)
|
39
|
+
@operation = operation
|
40
|
+
@response = response
|
41
|
+
super(*args)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
include ResultBase
|
46
|
+
end
|
47
|
+
|
48
|
+
class SyncResult
|
49
|
+
module ResultTypeBase
|
50
|
+
attr_reader :sync
|
51
|
+
|
52
|
+
def self.included(base)
|
53
|
+
base.class_eval do
|
54
|
+
serialize only: %i[sync]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def initialize(*args, sync:, **keywords)
|
59
|
+
@sync = sync
|
60
|
+
super(*args, **keywords)
|
61
|
+
end
|
62
|
+
|
63
|
+
def operations
|
64
|
+
@operations ||= sync.operations
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
include ResultBase
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
class SearchResult
|
73
|
+
module ResultTypeBase
|
74
|
+
attr_reader :resources, :searcher
|
75
|
+
|
76
|
+
def self.included(base)
|
77
|
+
base.class_eval do
|
78
|
+
serialize only: %i[
|
79
|
+
next_searcher
|
80
|
+
previous_searcher
|
81
|
+
resources
|
82
|
+
searcher
|
83
|
+
]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.included(base)
|
88
|
+
base.class_eval do
|
89
|
+
# TODO: removed next and previous searcher, because it causes a string of them. We should add next_searcher_params which would be easier to serialize.
|
90
|
+
serialize only: %i[resources searcher]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def initialize(*args, searcher:, **keywords)
|
95
|
+
@resources = searcher.resources
|
96
|
+
@searcher = searcher
|
97
|
+
super(*args, **keywords)
|
98
|
+
end
|
99
|
+
|
100
|
+
def next_searcher
|
101
|
+
searcher.next_searcher
|
102
|
+
end
|
103
|
+
|
104
|
+
def next_searcher?
|
105
|
+
!next_searcher.nil?
|
106
|
+
end
|
107
|
+
|
108
|
+
def previous_searcher
|
109
|
+
searcher.previous_searcher
|
110
|
+
end
|
111
|
+
|
112
|
+
def previous_searcher?
|
113
|
+
!previous_searcher.nil?
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
include ResultBase
|
118
|
+
end
|
119
|
+
|
120
|
+
class ValidationResult
|
121
|
+
module ResultTypeBase
|
122
|
+
attr_reader :validator
|
123
|
+
|
124
|
+
def self.included(base)
|
125
|
+
base.class_eval do
|
126
|
+
serialize only: %i[validator]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def initialize(validator:)
|
131
|
+
raise 'The argument must be a validator' unless validator.is_a?(Util::Validator)
|
132
|
+
|
133
|
+
@validator = validator
|
134
|
+
super(validator)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
include ResultBase
|
139
|
+
end
|
140
|
+
end
|