vindi-rails-integrations 0.3.0 → 0.4.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/README.md +19 -0
- data/lib/generators/vindi/outbox_generator.rb +24 -0
- data/lib/generators/vindi/templates/outbox_migration.rb +20 -0
- data/lib/vindi/integrations/concerns/synchronizable.rb +121 -65
- data/lib/vindi/integrations/pending_sync.rb +20 -0
- data/lib/vindi/integrations/version.rb +1 -1
- data/lib/vindi/jobs/process_pending_syncs_job.rb +73 -0
- data/lib/vindi-rails-integrations.rb +19 -13
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dcb347a77cfd8b20150b4469c8c2a0d5d18f09a95f832f5bd945ba72a3db44e4
|
|
4
|
+
data.tar.gz: ed3bc0bf9c62a9eb40c621573f66b92f094a924e98d0e6ebe07a67601d1b73d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5b72856876005e684c701c7fb001826210c726d827b8bfa802537131b3d9e0b77a6086bc1659d9a69c8a4c210665e4944e116851e8ae548f0c53a5bca3991e6
|
|
7
|
+
data.tar.gz: 897c26195875b0b4e213b8f80b01e3761820ca0a694d0f1f5f4490b0c361b9bc4e6d4831a043e9af7032d2811fbe9adf57e9f6ee6b942d9b2cb3ccd8c287107d
|
data/README.md
CHANGED
|
@@ -46,6 +46,25 @@ bundle exec rails generate vindi:sync User
|
|
|
46
46
|
```
|
|
47
47
|
This generates a database migration to add `vindi_customer_id` and includes the `Vindi::Synchronizable` concern into your model.
|
|
48
48
|
|
|
49
|
+
#### Resilient Transactional Outbox Sync (Optional)
|
|
50
|
+
To prevent network latencies or API downtime from blocking your local database transactions, you can enable the Outbox pattern. This saves the synchronization tasks locally in a database table during the transaction, and processes them asynchronously.
|
|
51
|
+
|
|
52
|
+
1. **Generate the Outbox migration**:
|
|
53
|
+
```bash
|
|
54
|
+
bundle exec rails generate vindi:outbox
|
|
55
|
+
bundle exec rails db:migrate
|
|
56
|
+
```
|
|
57
|
+
2. **Enable Outbox** in your initializer:
|
|
58
|
+
```ruby
|
|
59
|
+
Vindi.configure do |config|
|
|
60
|
+
config.use_outbox = true
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
3. **Processing**: The `Vindi::ProcessPendingSyncsJob` is automatically queued to run in the background after the model commits. You can also run it manually or schedule it:
|
|
64
|
+
```ruby
|
|
65
|
+
Vindi::ProcessPendingSyncsJob.perform_later
|
|
66
|
+
```
|
|
67
|
+
|
|
49
68
|
### 3. Rake Tasks
|
|
50
69
|
- **`bundle exec rake vindi:status`**: Verifies API configuration, environment, credentials (safely masked), and tests connection to Vindi.
|
|
51
70
|
- **`bundle exec rake vindi:audit model=User`**: Reconciles database records against the Vindi API to detect missing or mismatched records.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Vindi
|
|
7
|
+
module Generators
|
|
8
|
+
class OutboxGenerator < ::Rails::Generators::Base
|
|
9
|
+
include ::Rails::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Creates migration to store transactional outbox pending synchronization tasks"
|
|
14
|
+
|
|
15
|
+
def self.next_migration_number(dirname)
|
|
16
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create_migration_file
|
|
20
|
+
migration_template "outbox_migration.rb", "db/migrate/create_vindi_pending_syncs.rb"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateVindiPendingSyncs < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
4
|
+
def change
|
|
5
|
+
create_table :vindi_pending_syncs do |t|
|
|
6
|
+
t.string :item_type, null: false
|
|
7
|
+
t.string :item_id, null: false
|
|
8
|
+
t.string :action, null: false
|
|
9
|
+
t.text :params
|
|
10
|
+
t.string :status, null: false, default: "pending"
|
|
11
|
+
t.integer :attempts, null: false, default: 0
|
|
12
|
+
t.text :last_error
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :vindi_pending_syncs, [:status, :attempts]
|
|
18
|
+
add_index :vindi_pending_syncs, [:item_type, :item_id]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -1,65 +1,121 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Vindi
|
|
4
|
-
module Synchronizable
|
|
5
|
-
extend ActiveSupport::Concern
|
|
6
|
-
|
|
7
|
-
included do
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vindi
|
|
4
|
+
module Synchronizable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
after_create :record_vindi_create_pending_sync, if: :use_vindi_outbox?
|
|
9
|
+
after_update :record_vindi_update_pending_sync, if: :should_record_vindi_update_sync?
|
|
10
|
+
|
|
11
|
+
after_commit :create_vindi_customer, on: :create, if: :should_sync_vindi_create?
|
|
12
|
+
after_commit :update_vindi_customer, on: :update, if: :should_sync_vindi_update?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def sync_vindi_customer!
|
|
16
|
+
return unless synchronizable?
|
|
17
|
+
return if vindi_customer_id?
|
|
18
|
+
|
|
19
|
+
params = vindi_customer_attributes
|
|
20
|
+
customer = Vindi::Customer.create(params)
|
|
21
|
+
|
|
22
|
+
update_columns(vindi_customer_id: customer.id.to_s)
|
|
23
|
+
rescue Vindi::Error => e
|
|
24
|
+
Rails.logger.error("Failed to sync customer to Vindi: #{e.message}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update_vindi_customer!
|
|
28
|
+
return unless synchronizable?
|
|
29
|
+
return unless vindi_customer_id?
|
|
30
|
+
|
|
31
|
+
params = vindi_customer_attributes
|
|
32
|
+
Vindi::Customer.update(vindi_customer_id, params)
|
|
33
|
+
rescue Vindi::Error => e
|
|
34
|
+
Rails.logger.error("Failed to update customer in Vindi: #{e.message}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def use_vindi_outbox?
|
|
38
|
+
synchronizable? && Vindi.configuration.respond_to?(:use_outbox) && Vindi.configuration.use_outbox
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def synchronizable?
|
|
42
|
+
respond_to?(:vindi_customer_id) && self.class.name != "Vindi::PendingSync"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def create_vindi_customer
|
|
48
|
+
return unless synchronizable?
|
|
49
|
+
if use_vindi_outbox?
|
|
50
|
+
enqueue_vindi_outbox_processing
|
|
51
|
+
else
|
|
52
|
+
sync_vindi_customer!
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def update_vindi_customer
|
|
57
|
+
return unless synchronizable?
|
|
58
|
+
if use_vindi_outbox?
|
|
59
|
+
enqueue_vindi_outbox_processing
|
|
60
|
+
else
|
|
61
|
+
update_vindi_customer!
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def should_sync_vindi_create?
|
|
66
|
+
synchronizable? && !vindi_customer_id?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def should_sync_vindi_update?
|
|
70
|
+
synchronizable? && vindi_customer_id? && (saved_change_to_name? || saved_change_to_email?)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def record_vindi_create_pending_sync
|
|
74
|
+
return unless synchronizable?
|
|
75
|
+
Vindi::PendingSync.create!(
|
|
76
|
+
item_type: self.class.name,
|
|
77
|
+
item_id: id,
|
|
78
|
+
action: "create",
|
|
79
|
+
params: vindi_customer_attributes,
|
|
80
|
+
status: "pending"
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def record_vindi_update_pending_sync
|
|
85
|
+
return unless synchronizable?
|
|
86
|
+
Vindi::PendingSync.create!(
|
|
87
|
+
item_type: self.class.name,
|
|
88
|
+
item_id: id,
|
|
89
|
+
action: "update",
|
|
90
|
+
params: vindi_customer_attributes,
|
|
91
|
+
status: "pending"
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def should_record_vindi_update_sync?
|
|
96
|
+
use_vindi_outbox? && should_sync_vindi_update?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def enqueue_vindi_outbox_processing
|
|
100
|
+
Vindi::ProcessPendingSyncsJob.perform_later
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Default attributes mapping. Override this in the ActiveRecord model
|
|
104
|
+
# to customize parameters sent to Vindi (like registry_code, phone, etc.)
|
|
105
|
+
def vindi_customer_attributes
|
|
106
|
+
{
|
|
107
|
+
name: try(:name),
|
|
108
|
+
email: try(:email),
|
|
109
|
+
code: id.to_s
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
module Vindi
|
|
115
|
+
module Integrations
|
|
116
|
+
# Auto-load the concern when rails loads
|
|
117
|
+
ActiveSupport.on_load(:active_record) do
|
|
118
|
+
include Vindi::Synchronizable
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vindi
|
|
4
|
+
class PendingSync < ActiveRecord::Base
|
|
5
|
+
self.table_name = "vindi_pending_syncs"
|
|
6
|
+
|
|
7
|
+
if ActiveRecord.version >= Gem::Version.new("7.1.0")
|
|
8
|
+
serialize :params, coder: JSON
|
|
9
|
+
else
|
|
10
|
+
serialize :params, JSON
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
validates :item_type, :item_id, :action, :status, presence: true
|
|
14
|
+
validates :status, inclusion: { in: %w[pending processing processed failed] }
|
|
15
|
+
|
|
16
|
+
scope :pending, -> { where(status: "pending") }
|
|
17
|
+
scope :failed, -> { where(status: "failed") }
|
|
18
|
+
scope :retryable, ->(max_attempts = 5) { where(status: %w[pending failed]).where("attempts < ?", max_attempts) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Vindi
|
|
4
|
+
class ProcessPendingSyncsJob < ActiveJob::Base
|
|
5
|
+
queue_as :default
|
|
6
|
+
|
|
7
|
+
def perform(pending_sync_id = nil)
|
|
8
|
+
if pending_sync_id
|
|
9
|
+
pending_sync = Vindi::PendingSync.find_by(id: pending_sync_id)
|
|
10
|
+
process_sync(pending_sync) if pending_sync
|
|
11
|
+
else
|
|
12
|
+
Vindi::PendingSync.retryable.find_each do |sync|
|
|
13
|
+
process_sync(sync)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def process_sync(pending_sync)
|
|
21
|
+
return unless retryable?(pending_sync)
|
|
22
|
+
|
|
23
|
+
pending_sync.update!(status: "processing")
|
|
24
|
+
item = find_local_item(pending_sync)
|
|
25
|
+
|
|
26
|
+
if item.nil?
|
|
27
|
+
pending_sync.update!(status: "failed", last_error: "Record not found locally: #{pending_sync.item_type}##{pending_sync.item_id}")
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
execute_api_call!(pending_sync, item)
|
|
32
|
+
pending_sync.update!(status: "processed", last_error: nil)
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
handle_sync_failure(pending_sync, e)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def retryable?(pending_sync)
|
|
38
|
+
%w[pending failed].include?(pending_sync.status)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def find_local_item(pending_sync)
|
|
42
|
+
pending_sync.item_type.constantize.find_by(id: pending_sync.item_id)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def execute_api_call!(pending_sync, item)
|
|
46
|
+
case pending_sync.action
|
|
47
|
+
when "create", "update"
|
|
48
|
+
sync_to_vindi(item.vindi_customer_id, pending_sync.params, item)
|
|
49
|
+
else
|
|
50
|
+
raise ArgumentError, "Unknown action #{pending_sync.action.inspect}. Expected 'create' or 'update'."
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def sync_to_vindi(customer_id, params, item)
|
|
55
|
+
if customer_id.present?
|
|
56
|
+
Vindi::Customer.update(customer_id, params)
|
|
57
|
+
else
|
|
58
|
+
customer = Vindi::Customer.create(params)
|
|
59
|
+
item.update_columns(vindi_customer_id: customer.id.to_s)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def handle_sync_failure(pending_sync, error)
|
|
64
|
+
new_attempts = pending_sync.attempts + 1
|
|
65
|
+
status = new_attempts >= 5 ? "failed" : "pending"
|
|
66
|
+
pending_sync.update!(
|
|
67
|
+
status: status,
|
|
68
|
+
attempts: new_attempts,
|
|
69
|
+
last_error: "#{error.class}: #{error.message}"
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "vindi"
|
|
4
|
-
require_relative "vindi/integrations/version"
|
|
5
|
-
require_relative "vindi/integrations/diagnostics"
|
|
6
|
-
require_relative "vindi/integrations/railtie" if defined?(Rails)
|
|
7
|
-
require_relative "vindi/integrations/concerns/synchronizable" if defined?(ActiveRecord)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "vindi"
|
|
4
|
+
require_relative "vindi/integrations/version"
|
|
5
|
+
require_relative "vindi/integrations/diagnostics"
|
|
6
|
+
require_relative "vindi/integrations/railtie" if defined?(Rails)
|
|
7
|
+
require_relative "vindi/integrations/concerns/synchronizable" if defined?(ActiveRecord)
|
|
8
|
+
require_relative "vindi/integrations/pending_sync" if defined?(ActiveRecord)
|
|
9
|
+
require_relative "vindi/jobs/process_pending_syncs_job" if defined?(ActiveJob)
|
|
10
|
+
|
|
11
|
+
module Vindi
|
|
12
|
+
class Configuration
|
|
13
|
+
attr_accessor :use_outbox
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module Integrations
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
end
|
|
19
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vindi-rails-integrations
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wesley Lima
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: vindi-rails
|
|
@@ -145,9 +145,11 @@ extensions: []
|
|
|
145
145
|
extra_rdoc_files: []
|
|
146
146
|
files:
|
|
147
147
|
- README.md
|
|
148
|
+
- lib/generators/vindi/outbox_generator.rb
|
|
148
149
|
- lib/generators/vindi/sync_generator.rb
|
|
149
150
|
- lib/generators/vindi/templates/base_handler.rb
|
|
150
151
|
- lib/generators/vindi/templates/migration.rb
|
|
152
|
+
- lib/generators/vindi/templates/outbox_migration.rb
|
|
151
153
|
- lib/generators/vindi/templates/webhook_handler.rb
|
|
152
154
|
- lib/generators/vindi/templates/webhook_job.rb
|
|
153
155
|
- lib/generators/vindi/templates/webhooks_controller.rb
|
|
@@ -157,8 +159,10 @@ files:
|
|
|
157
159
|
- lib/vindi-rails-integrations.rb
|
|
158
160
|
- lib/vindi/integrations/concerns/synchronizable.rb
|
|
159
161
|
- lib/vindi/integrations/diagnostics.rb
|
|
162
|
+
- lib/vindi/integrations/pending_sync.rb
|
|
160
163
|
- lib/vindi/integrations/railtie.rb
|
|
161
164
|
- lib/vindi/integrations/version.rb
|
|
165
|
+
- lib/vindi/jobs/process_pending_syncs_job.rb
|
|
162
166
|
homepage: https://github.com/wesleyskap/vindi-rails-integrations
|
|
163
167
|
licenses:
|
|
164
168
|
- MIT
|