ledger_sync 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|