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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac62896e691574d5b96e6ed7b00e2456222db8b7d575393199e1e5d2f982aaaf
4
- data.tar.gz: 95876321b904170ac20f12163240fa140fecf3747f3383e725220b16fdf8b7e9
3
+ metadata.gz: dcb347a77cfd8b20150b4469c8c2a0d5d18f09a95f832f5bd945ba72a3db44e4
4
+ data.tar.gz: ed3bc0bf9c62a9eb40c621573f66b92f094a924e98d0e6ebe07a67601d1b73d1
5
5
  SHA512:
6
- metadata.gz: 7dfcb70a8753f6a6272b82d4e8f71d23542a4400e45b4b5860b9291aeef90424ffe5178d1e47e7c2da3d06178b90f8ef086da7dbe067ef9213650d2805f288f9
7
- data.tar.gz: 9d1ab6461d8d9e41e8825b6c1d4fc521496c80ee68dca3d27d76ac04f99e30de7e3fad58b10ed156011baa37aa423e877c7fd6eabe33493202e7afb476892aa7
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
- after_commit :create_vindi_customer, on: :create, unless: :vindi_customer_id?
9
- after_commit :update_vindi_customer, on: :update, if: :should_sync_vindi_update?
10
- end
11
-
12
- def sync_vindi_customer!
13
- return if vindi_customer_id?
14
-
15
- params = vindi_customer_attributes
16
- customer = Vindi::Customer.create(params)
17
-
18
- update_columns(vindi_customer_id: customer.id.to_s)
19
- rescue Vindi::Error => e
20
- Rails.logger.error("Failed to sync customer to Vindi: #{e.message}")
21
- end
22
-
23
- def update_vindi_customer!
24
- return unless vindi_customer_id?
25
-
26
- params = vindi_customer_attributes
27
- Vindi::Customer.update(vindi_customer_id, params)
28
- rescue Vindi::Error => e
29
- Rails.logger.error("Failed to update customer in Vindi: #{e.message}")
30
- end
31
-
32
- private
33
-
34
- def create_vindi_customer
35
- # Run in background or synchronously based on configuration
36
- sync_vindi_customer!
37
- end
38
-
39
- def update_vindi_customer
40
- update_vindi_customer!
41
- end
42
-
43
- def should_sync_vindi_update?
44
- vindi_customer_id? && (saved_change_to_name? || saved_change_to_email?)
45
- end
46
-
47
- # Default attributes mapping. Override this in the ActiveRecord model
48
- # to customize parameters sent to Vindi (like registry_code, phone, etc.)
49
- def vindi_customer_attributes
50
- {
51
- name: try(:name),
52
- email: try(:email),
53
- code: id.to_s
54
- }
55
- end
56
- end
57
- end
58
- module Vindi
59
- module Integrations
60
- # Auto-load the concern when rails loads
61
- ActiveSupport.on_load(:active_record) do
62
- include Vindi::Synchronizable
63
- end
64
- end
65
- end
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Vindi
4
4
  module Integrations
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  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
- module Vindi
10
- module Integrations
11
- class Error < StandardError; end
12
- end
13
- end
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.3.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-12 00:00:00.000000000 Z
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