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.
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