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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.gitignore +4 -0
  6. data/.rubocop.yml +4 -0
  7. data/.rubocop_todo.yml +25 -0
  8. data/.travis.yml +1 -1
  9. data/Dockerfile +8 -0
  10. data/Gemfile +3 -1
  11. data/Gemfile.lock +137 -8
  12. data/README.md +184 -9
  13. data/Rakefile +3 -3
  14. data/bin/console +3 -3
  15. data/docker-compose.yml +4 -0
  16. data/ledger_sync.gemspec +32 -11
  17. data/lib/ledger_sync.rb +115 -3
  18. data/lib/ledger_sync/adaptor_configuration.rb +55 -0
  19. data/lib/ledger_sync/adaptor_configuration_store.rb +52 -0
  20. data/lib/ledger_sync/adaptors/adaptor.rb +66 -0
  21. data/lib/ledger_sync/adaptors/contract.rb +16 -0
  22. data/lib/ledger_sync/adaptors/operation.rb +213 -0
  23. data/lib/ledger_sync/adaptors/quickbooks_online/adaptor.rb +161 -0
  24. data/lib/ledger_sync/adaptors/quickbooks_online/config.rb +7 -0
  25. data/lib/ledger_sync/adaptors/quickbooks_online/customer/operations/create.rb +44 -0
  26. data/lib/ledger_sync/adaptors/quickbooks_online/customer/operations/find.rb +35 -0
  27. data/lib/ledger_sync/adaptors/quickbooks_online/customer/operations/update.rb +53 -0
  28. data/lib/ledger_sync/adaptors/quickbooks_online/customer/operations/upsert.rb +42 -0
  29. data/lib/ledger_sync/adaptors/quickbooks_online/customer/searcher.rb +63 -0
  30. data/lib/ledger_sync/adaptors/quickbooks_online/invoice/operations/create.rb +63 -0
  31. data/lib/ledger_sync/adaptors/quickbooks_online/invoice/operations/find.rb +36 -0
  32. data/lib/ledger_sync/adaptors/quickbooks_online/invoice/operations/update.rb +67 -0
  33. data/lib/ledger_sync/adaptors/quickbooks_online/invoice/operations/upsert.rb +44 -0
  34. data/lib/ledger_sync/adaptors/quickbooks_online/payment/operations/create.rb +64 -0
  35. data/lib/ledger_sync/adaptors/quickbooks_online/payment/operations/find.rb +35 -0
  36. data/lib/ledger_sync/adaptors/quickbooks_online/payment/operations/update.rb +64 -0
  37. data/lib/ledger_sync/adaptors/quickbooks_online/payment/operations/upsert.rb +53 -0
  38. data/lib/ledger_sync/adaptors/quickbooks_online/product/operations/create.rb +46 -0
  39. data/lib/ledger_sync/adaptors/quickbooks_online/product/operations/find.rb +34 -0
  40. data/lib/ledger_sync/adaptors/quickbooks_online/product/operations/update.rb +50 -0
  41. data/lib/ledger_sync/adaptors/quickbooks_online/product/operations/upsert.rb +43 -0
  42. data/lib/ledger_sync/adaptors/quickbooks_online/util/adaptor_error_parser.rb +102 -0
  43. data/lib/ledger_sync/adaptors/quickbooks_online/util/error_matcher.rb +54 -0
  44. data/lib/ledger_sync/adaptors/quickbooks_online/util/error_parser.rb +27 -0
  45. data/lib/ledger_sync/adaptors/quickbooks_online/util/operation_error_parser.rb +96 -0
  46. data/lib/ledger_sync/adaptors/searcher.rb +64 -0
  47. data/lib/ledger_sync/adaptors/test/adaptor.rb +47 -0
  48. data/lib/ledger_sync/adaptors/test/config.rb +7 -0
  49. data/lib/ledger_sync/adaptors/test/customer/operations/create.rb +49 -0
  50. data/lib/ledger_sync/adaptors/test/customer/operations/find.rb +35 -0
  51. data/lib/ledger_sync/adaptors/test/customer/operations/invalid.rb +20 -0
  52. data/lib/ledger_sync/adaptors/test/customer/operations/update.rb +46 -0
  53. data/lib/ledger_sync/adaptors/test/customer/operations/upsert.rb +42 -0
  54. data/lib/ledger_sync/adaptors/test/customer/operations/valid.rb +26 -0
  55. data/lib/ledger_sync/adaptors/test/customer/searcher.rb +40 -0
  56. data/lib/ledger_sync/adaptors/test/error/adaptor_error/operations/throttle_error.rb +28 -0
  57. data/lib/ledger_sync/adaptors/test/payment/operations/create.rb +56 -0
  58. data/lib/ledger_sync/adaptors/test/payment/operations/find.rb +35 -0
  59. data/lib/ledger_sync/adaptors/test/payment/operations/update.rb +62 -0
  60. data/lib/ledger_sync/adaptors/test/payment/operations/upsert.rb +53 -0
  61. data/lib/ledger_sync/concerns/validatable.rb +19 -0
  62. data/lib/ledger_sync/core_ext/resonad.rb +16 -0
  63. data/lib/ledger_sync/error.rb +10 -0
  64. data/lib/ledger_sync/error/adaptor_errors.rb +47 -0
  65. data/lib/ledger_sync/error/operation_errors.rb +41 -0
  66. data/lib/ledger_sync/error/resource_errors.rb +20 -0
  67. data/lib/ledger_sync/resource.rb +92 -0
  68. data/lib/ledger_sync/resources/customer.rb +8 -0
  69. data/lib/ledger_sync/resources/invoice.rb +12 -0
  70. data/lib/ledger_sync/resources/payment.rb +11 -0
  71. data/lib/ledger_sync/resources/product.rb +6 -0
  72. data/lib/ledger_sync/resources/vendor.rb +6 -0
  73. data/lib/ledger_sync/result.rb +140 -0
  74. data/lib/ledger_sync/sync.rb +107 -0
  75. data/lib/ledger_sync/util/coordinator.rb +72 -0
  76. data/lib/ledger_sync/util/debug.rb +16 -0
  77. data/lib/ledger_sync/util/hash_helpers.rb +13 -0
  78. data/lib/ledger_sync/util/performer.rb +29 -0
  79. data/lib/ledger_sync/util/resources_builder.rb +68 -0
  80. data/lib/ledger_sync/util/string_helpers.rb +37 -0
  81. data/lib/ledger_sync/util/validator.rb +49 -0
  82. data/lib/ledger_sync/version.rb +1 -1
  83. data/release.sh +8 -0
  84. metadata +334 -11
  85. 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,10 @@
1
+ module LedgerSync
2
+ class Error < StandardError
3
+ attr_reader :message
4
+
5
+ def initialize(message:)
6
+ @message = message
7
+ super(message)
8
+ end
9
+ end
10
+ 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,8 @@
1
+ module LedgerSync
2
+ class Customer < LedgerSync::Resource
3
+ attribute :email
4
+ attribute :name
5
+ attribute :phone_number
6
+ # attribute :active
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ module LedgerSync
2
+ class Invoice < LedgerSync::Resource
3
+ attribute :customer
4
+ attribute :number
5
+ attribute :line_items
6
+ attribute :currency
7
+
8
+ def name
9
+ "Invoice ##{number}"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module LedgerSync
2
+ class Payment < LedgerSync::Resource
3
+ attribute :currency
4
+ reference :customer, Customer
5
+ attribute :amount
6
+
7
+ def name
8
+ "Payment: #{amount} #{currency}"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module LedgerSync
2
+ class Product < LedgerSync::Resource
3
+ attribute :name
4
+ attribute :description
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module LedgerSync
2
+ class Vendor < LedgerSync::Resource
3
+ attribute :email
4
+ attribute :name
5
+ end
6
+ 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