vindi-rails-integrations 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 40f92c55d8ade3ac7811b05e7e0b25e202f59eee0b87eb4a9d54994a0448ec52
4
+ data.tar.gz: fbdae32b900767b7545db87488bd3f06c0b28f0971a93ce1869e223145734c22
5
+ SHA512:
6
+ metadata.gz: ad7b6774270fd3e97f67a231a040ad4cee7145b301ff3979e36483719d66e06b5650e351fd85317fa63bc7511cd81528dba8977b1c4989701886de6238963cd3
7
+ data.tar.gz: fda62e2338d8500ef9d4efb909e04697b66198f6830d0ec06e471ecfd7e722c1cd41ac0bdf5f3892279d8fb7d2a3e7fcb44d7368cf5afde76155b92c5a7f4f09
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # Vindi Rails Integrations
2
+
3
+ [Leia em Português (README.pt-BR.md)](./README.pt-BR.md)
4
+
5
+ An extension gem for the [vindi-rails](https://github.com/wesleyskap/vindi-rails) core SDK, providing backend integrations such as automatic ActiveRecord model synchronization, webhook controller endpoints, asynchronous processing jobs, and verification Rake tasks.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'vindi-rails-integrations'
13
+ ```
14
+
15
+ ## Features & Usage
16
+
17
+ ### 1. Webhook Setup
18
+ To handle incoming webhook events asynchronously with built-in access token verification:
19
+ ```bash
20
+ bundle exec rails generate vindi:webhook
21
+ ```
22
+ This generates:
23
+ - `Vindi::WebhooksController` (`app/controllers/vindi/webhooks_controller.rb`)
24
+ - `Vindi::WebhookJob` (`app/jobs/vindi/webhook_job.rb`)
25
+
26
+ Configure your webhook access token in your environment files:
27
+ ```bash
28
+ ENV["VINDI_WEBHOOK_TOKEN"] = "YOUR_SECURE_TOKEN"
29
+ ```
30
+
31
+ ### 2. ActiveRecord Model Sync
32
+ To automatically synchronize local models (e.g. `User`) with Vindi Customers:
33
+ ```bash
34
+ bundle exec rails generate vindi:sync User
35
+ ```
36
+ This generates a database migration to add `vindi_customer_id` and includes the `Vindi::Synchronizable` concern into your model.
37
+
38
+ ### 3. Rake Tasks
39
+ - **`bundle exec rake vindi:audit model=User`**: Reconciles database records against the Vindi API to detect missing or mismatched records.
40
+ - **`bundle exec rake vindi:test_webhook event=bill_paid`**: Simulates sending a webhook event payload directly to your local endpoint.
41
+
42
+ ## Running Tests
43
+
44
+ To run the Minitest suite:
45
+ ```bash
46
+ bundle exec rake test
47
+ ```
@@ -0,0 +1,40 @@
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 SyncGenerator < ::Rails::Generators::NamedBase
9
+ include ::Rails::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Adds Vindi synchronization columns to database and concern to your model."
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 "migration.rb", "db/migrate/add_vindi_fields_to_#{table_name}.rb"
21
+ end
22
+
23
+ def inject_concern_into_model
24
+ model_path = File.join("app/models", "#{file_path}.rb")
25
+
26
+ if File.exist?(model_path)
27
+ inject_into_class(model_path, class_name, " include Vindi::Synchronizable\n")
28
+ else
29
+ say_status("warning", "Model file #{model_path} not found. Please add 'include Vindi::Synchronizable' manually.", :yellow)
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def table_name
36
+ name.pluralize.underscore
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddVindiFieldsTo<%= class_name.pluralize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
+ def change
5
+ add_column :<%= table_name %>, :vindi_customer_id, :string
6
+ add_column :<%= table_name %>, :vindi_payment_profile_id, :string
7
+
8
+ add_index :<%= table_name %>, :vindi_customer_id, unique: true
9
+ end
10
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # =========================================================================
5
+ # VINDI WEBHOOK PAYLOAD STRUCTURE EXAMPLE:
6
+ # {
7
+ # event: {
8
+ # id: 123456, # Unique ID for the webhook event (useful for idempotency)
9
+ # type: "bill_paid", # Type of event (e.g. subscription_created, bill_paid, charge_rejected)
10
+ # created_at: "2026-06-10T15:00:00.000-03:00",
11
+ # data: { # Object data (context depends on the event type)
12
+ # bill: {
13
+ # id: 555,
14
+ # amount: "100.00",
15
+ # status: "paid",
16
+ # customer: { id: 123, email: "john@example.com", code: "app_user_id" },
17
+ # charges: [...]
18
+ # }
19
+ # }
20
+ # }
21
+ # }
22
+ # =========================================================================
23
+ class WebhookJob < ActiveJob::Base
24
+ queue_as :default
25
+
26
+ def perform(payload)
27
+ event_id = payload.dig(:event, :id)
28
+ event_type = payload.dig(:event, :type)
29
+
30
+ # BEST PRACTICE 1: Idempotency Check
31
+ # Check if this event_id has already been processed to avoid duplicate actions.
32
+ return if already_processed?(event_id)
33
+
34
+ process_event(event_type, payload.dig(:event, :data))
35
+
36
+ # BEST PRACTICE 2: Record that this event was successfully processed
37
+ mark_as_processed!(event_id)
38
+ end
39
+
40
+ private
41
+
42
+ def process_event(event_type, data)
43
+ case event_type
44
+ when "subscription_created"
45
+ handle_subscription_created(data[:subscription])
46
+ when "bill_paid"
47
+ handle_bill_paid(data[:bill])
48
+ else
49
+ logger.info "Unhandled Vindi webhook event: #{event_type}"
50
+ end
51
+ end
52
+
53
+ # BEST PRACTICE 3: Safe target state validations (e.g. ignore if already active/paid)
54
+ def handle_subscription_created(subscription_data)
55
+ return if subscription_data.nil?
56
+ # e.g., Find local tenant/user using subscription_data[:customer][:code]
57
+ # and activate their account.
58
+ end
59
+
60
+ def handle_bill_paid(bill_data)
61
+ return if bill_data.nil?
62
+ # e.g., Find local invoice by bill_data[:id]
63
+ # and mark as paid. Check first if it isn't already paid.
64
+ end
65
+
66
+ def already_processed?(event_id)
67
+ # e.g., VindiWebhookEvent.exists?(vindi_event_id: event_id)
68
+ false
69
+ end
70
+
71
+ def mark_as_processed!(event_id)
72
+ # e.g., VindiWebhookEvent.create!(vindi_event_id: event_id, processed_at: Time.current)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ class WebhooksController < ActionController::API
5
+ before_action :verify_token!
6
+
7
+ def create
8
+ payload = JSON.parse(request.body.read, symbolize_names: true)
9
+
10
+ # Enqueue Vindi Webhook processing
11
+ Vindi::WebhookJob.perform_later(payload)
12
+
13
+ render json: { status: "received" }, status: :ok
14
+ rescue JSON::ParserError
15
+ render json: { error: "Invalid payload" }, status: :bad_request
16
+ end
17
+
18
+ private
19
+
20
+ # SECURITY BEST PRACTICE: Verify the authenticity of the request.
21
+ # Configure your Webhook URL in the Vindi Dashboard with a secret query token, e.g.:
22
+ # https://yourdomain.com/vindi/webhooks?token=YOUR_SECURE_TOKEN
23
+ def verify_token!
24
+ token = params[:token]
25
+ expected_token = ENV["VINDI_WEBHOOK_TOKEN"]
26
+
27
+ if expected_token.blank? || token != expected_token
28
+ render json: { error: "Unauthorized access token" }, status: :unauthorized
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Vindi
6
+ module Generators
7
+ class WebhookGenerator < ::Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Creates a Webhooks controller and an ActiveJob to handle Vindi webhook events."
11
+
12
+ def copy_webhook_files
13
+ template "webhooks_controller.rb", "app/controllers/vindi/webhooks_controller.rb"
14
+ template "webhook_job.rb", "app/jobs/vindi/webhook_job.rb"
15
+ route 'post "/vindi/webhooks" => "vindi/webhooks#create"'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :vindi do
4
+ desc "Audit and reconcile local model records with Vindi Customers"
5
+ task audit: :environment do
6
+ model_name = ENV["model"] || "User"
7
+ klass = model_name.constantize
8
+ raise "Model #{model_name} is not synchronizable" unless klass.include?(Vindi::Synchronizable)
9
+
10
+ audit_records(klass)
11
+ end
12
+
13
+ desc "Simulate a Vindi webhook event locally"
14
+ task test_webhook: :environment do
15
+ event_type = ENV["event"] || "bill_paid"
16
+ url = ENV["url"] || "http://localhost:3000/vindi/webhooks"
17
+ token = ENV["token"] || ENV["VINDI_WEBHOOK_TOKEN"]
18
+
19
+ payload = build_simulated_payload(event_type)
20
+ send_simulated_webhook(url, token, payload)
21
+ end
22
+ end
23
+
24
+ def audit_records(klass)
25
+ puts "Auditing #{klass.name} records..."
26
+ klass.where.not(vindi_customer_id: nil).find_each do |record|
27
+ Vindi::Customer.find(record.vindi_customer_id)
28
+ puts "Record #{record.id}: OK (Synced with Vindi customer #{record.vindi_customer_id})"
29
+ rescue Vindi::NotFoundError
30
+ puts "Record #{record.id}: ERROR - Customer #{record.vindi_customer_id} not found in Vindi!"
31
+ rescue Vindi::Error => e
32
+ puts "Record #{record.id}: API Error - #{e.message}"
33
+ end
34
+ end
35
+
36
+ def build_simulated_payload(event_type)
37
+ {
38
+ event: {
39
+ id: rand(100_000..999_999),
40
+ type: event_type,
41
+ created_at: Time.current.iso8601,
42
+ data: {
43
+ bill: { id: 555, amount: "100.00", status: "paid" },
44
+ subscription: { id: 777, status: "active" }
45
+ }
46
+ }
47
+ }
48
+ end
49
+
50
+ def send_simulated_webhook(url, token, payload)
51
+ full_url = token ? "#{url}?token=#{token}" : url
52
+ puts "Sending simulated webhook to #{full_url}..."
53
+
54
+ response = RestClient.post(
55
+ full_url,
56
+ payload.to_json,
57
+ { content_type: :json, accept: :json }
58
+ )
59
+ puts "Response Code: #{response.code}"
60
+ puts "Response Body: #{response.body}"
61
+ rescue RestClient::Exception => e
62
+ puts "Failed: #{e.message}"
63
+ puts "Response: #{e.response&.body}"
64
+ end
@@ -0,0 +1,65 @@
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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ module Integrations
5
+ class Railtie < ::Rails::Railtie
6
+ # Automatic loading of generators and integrations in Rails
7
+ rake_tasks do
8
+ load File.expand_path("../../../tasks/vindi/tasks.rake", __dir__)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ module Integrations
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "vindi"
4
+ require_relative "vindi/integrations/version"
5
+ require_relative "vindi/integrations/railtie" if defined?(Rails)
6
+ require_relative "vindi/integrations/concerns/synchronizable" if defined?(ActiveRecord)
7
+
8
+ module Vindi
9
+ module Integrations
10
+ class Error < StandardError; end
11
+ end
12
+ end
metadata ADDED
@@ -0,0 +1,183 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vindi-rails-integrations
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Wesley Lima
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: vindi-rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: railties
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '6.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: activerecord
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '6.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '6.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: activejob
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '6.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '6.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: actionpack
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '6.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '6.0'
139
+ description: Provides webhook handling, background jobs, and data synchronization
140
+ for the Vindi Rails SDK.
141
+ email:
142
+ - wesleyskap@gmail.com
143
+ executables: []
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - README.md
148
+ - lib/generators/vindi/sync_generator.rb
149
+ - lib/generators/vindi/templates/migration.rb
150
+ - lib/generators/vindi/templates/webhook_job.rb
151
+ - lib/generators/vindi/templates/webhooks_controller.rb
152
+ - lib/generators/vindi/webhook_generator.rb
153
+ - lib/tasks/vindi/tasks.rake
154
+ - lib/vindi-rails-integrations.rb
155
+ - lib/vindi/integrations/concerns/synchronizable.rb
156
+ - lib/vindi/integrations/railtie.rb
157
+ - lib/vindi/integrations/version.rb
158
+ homepage: https://github.com/wesleyskap/vindi-rails
159
+ licenses:
160
+ - MIT
161
+ metadata:
162
+ homepage_uri: https://github.com/wesleyskap/vindi-rails
163
+ source_code_uri: https://github.com/wesleyskap/vindi-rails
164
+ post_install_message:
165
+ rdoc_options: []
166
+ require_paths:
167
+ - lib
168
+ required_ruby_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: 3.0.0
173
+ required_rubygems_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
178
+ requirements: []
179
+ rubygems_version: 3.5.22
180
+ signing_key:
181
+ specification_version: 4
182
+ summary: Rails backend integrations (webhooks, jobs, sync) for Vindi API.
183
+ test_files: []