vindi-hermes 0.0.1
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 +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +100 -0
- data/LICENSE.txt +21 -0
- data/README.md +207 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/vindi.rb +76 -0
- data/lib/vindi/core_extensions/her_with_query_filter.rb +67 -0
- data/lib/vindi/middleware/rate_limit_validation.rb +23 -0
- data/lib/vindi/middleware/response_parser.rb +50 -0
- data/lib/vindi/models/address.rb +9 -0
- data/lib/vindi/models/bill.rb +26 -0
- data/lib/vindi/models/bill_item.rb +18 -0
- data/lib/vindi/models/charge.rb +9 -0
- data/lib/vindi/models/customer.rb +29 -0
- data/lib/vindi/models/discount.rb +8 -0
- data/lib/vindi/models/issue.rb +8 -0
- data/lib/vindi/models/model.rb +105 -0
- data/lib/vindi/models/notification.rb +8 -0
- data/lib/vindi/models/payment_method.rb +8 -0
- data/lib/vindi/models/payment_profile.rb +9 -0
- data/lib/vindi/models/period.rb +8 -0
- data/lib/vindi/models/plan.rb +90 -0
- data/lib/vindi/models/plan_item.rb +11 -0
- data/lib/vindi/models/pricing_schema.rb +8 -0
- data/lib/vindi/models/product.rb +25 -0
- data/lib/vindi/models/product_item.rb +8 -0
- data/lib/vindi/models/subscription.rb +32 -0
- data/lib/vindi/models/transaction.rb +8 -0
- data/lib/vindi/rate_limit.rb +18 -0
- data/lib/vindi/version.rb +5 -0
- data/vindi-hermes.gemspec +32 -0
- metadata +150 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "vindi"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/vindi.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
require "faraday_middleware"
|
5
|
+
require "her"
|
6
|
+
|
7
|
+
require_relative "vindi/version"
|
8
|
+
require_relative "vindi/rate_limit"
|
9
|
+
require_relative "vindi/middleware/rate_limit_validation"
|
10
|
+
require_relative "vindi/middleware/response_parser"
|
11
|
+
require_relative "vindi/core_extensions/her_with_query_filter"
|
12
|
+
|
13
|
+
module Vindi
|
14
|
+
class Error < StandardError; end
|
15
|
+
|
16
|
+
RESOURCE_MODELS = Dir[File.expand_path("vindi/models/**/*.rb", File.dirname(__FILE__))].freeze
|
17
|
+
|
18
|
+
RESOURCE_MODELS.each do |f|
|
19
|
+
autoload File.basename(f, ".rb").camelcase.to_sym, f
|
20
|
+
end
|
21
|
+
|
22
|
+
# Set sandbox to true in dev mode.
|
23
|
+
mattr_accessor :sandbox
|
24
|
+
@@sandbox = false
|
25
|
+
|
26
|
+
# Set the API KEY to assign the API calls.
|
27
|
+
mattr_accessor :api_key
|
28
|
+
@@api_key = false
|
29
|
+
|
30
|
+
# Validates incoming Vindi Webhook calls with the given secret name.
|
31
|
+
mattr_accessor :webhook_name
|
32
|
+
@@webhook_name = nil
|
33
|
+
|
34
|
+
# Validates incoming Vindi Webhook calls with the given secret password.
|
35
|
+
mattr_accessor :webhook_password
|
36
|
+
@@webhook_password = nil
|
37
|
+
|
38
|
+
VINDI_API_URL = "https://app.vindi.com.br/api/v1"
|
39
|
+
VINDI_SANDBOX_API_URL = "https://sandbox-app.vindi.com.br/api/v1"
|
40
|
+
|
41
|
+
def self.api_url
|
42
|
+
return VINDI_SANDBOX_API_URL if @@sandbox
|
43
|
+
|
44
|
+
VINDI_API_URL
|
45
|
+
end
|
46
|
+
|
47
|
+
# @example
|
48
|
+
# Vindi.setup do |c|
|
49
|
+
# c.sandbox = true
|
50
|
+
# c.api_key = 'MY API KEY'
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
def self.config
|
54
|
+
yield self
|
55
|
+
|
56
|
+
her_setup
|
57
|
+
end
|
58
|
+
|
59
|
+
# @private
|
60
|
+
def self.her_setup
|
61
|
+
Her::API.setup url: Vindi.api_url do |conn|
|
62
|
+
conn.basic_auth Vindi.api_key, ""
|
63
|
+
|
64
|
+
# Request
|
65
|
+
conn.use ::Vindi::Middleware::RateLimitValidation
|
66
|
+
conn.request :json
|
67
|
+
|
68
|
+
# Response
|
69
|
+
conn.response :json, content_type: /\bjson$/
|
70
|
+
conn.use ::Vindi::Middleware::ResponseParser
|
71
|
+
|
72
|
+
# Adapter
|
73
|
+
conn.adapter Faraday.default_adapter
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CoreExtensions
|
4
|
+
# WARNING: monkey patch (https://github.com/remi/her/blob/master/lib/her/model/relation.rb#L34)
|
5
|
+
#
|
6
|
+
# The `where` clause must be adapted to vindi requirements:
|
7
|
+
# https://atendimento.vindi.com.br/hc/pt-br/articles/204163150
|
8
|
+
module HerWithQueryFilter
|
9
|
+
# Add a query string parameter
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# @users = User.where(contains: { name: 'Gandalf' })
|
13
|
+
# # Fetched via GET "/users?query=name:Gandalf"
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# @users = User.active.where(gt: { created_at: Time.zone.yesterday })
|
17
|
+
# # Fetched via GET "/users?query=status=active created_at>2021-01-01"
|
18
|
+
def where(params = {})
|
19
|
+
return self if params.blank? && !@_fetch.nil?
|
20
|
+
|
21
|
+
clone.tap do |r|
|
22
|
+
default_params = extract_params params, r.params
|
23
|
+
query = params_to_query params, parse_query(r.params.fetch(:query, ""))
|
24
|
+
|
25
|
+
r.params = r.params.merge default_params.merge(query: query)
|
26
|
+
r.clear_fetch_cache!
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def parse_query(query)
|
33
|
+
CGI.parse(query).transform_values(&:first)
|
34
|
+
end
|
35
|
+
|
36
|
+
def extract_params(new_ones, old_ones)
|
37
|
+
page = new_ones.delete(:page) || old_ones.delete(:page) || 1
|
38
|
+
per_page = new_ones.delete(:per_page) || old_ones.delete(:per_page) || 25
|
39
|
+
sort_by = new_ones.delete(:sort_by) || old_ones.delete(:sort_by) || :created_at
|
40
|
+
sort_order = new_ones.delete(:sort_order) || old_ones.delete(:sort_order) || :desc
|
41
|
+
|
42
|
+
{
|
43
|
+
page: page,
|
44
|
+
per_page: per_page,
|
45
|
+
sort_by: sort_by,
|
46
|
+
sort_order: sort_order
|
47
|
+
}.delete_if { |_, v| v.nil? }
|
48
|
+
end
|
49
|
+
|
50
|
+
def params_to_query(params, query)
|
51
|
+
params.merge(query).map do |key, value|
|
52
|
+
case key
|
53
|
+
when :contains then value.map { |k, v| "#{k}:#{v}" }.last
|
54
|
+
when :gt then value.map { |k, v| "#{k}>#{v}" }.last
|
55
|
+
when :gteq then value.map { |k, v| "#{k}>=#{v}" }.last
|
56
|
+
when :lt then value.map { |k, v| "#{k}<#{v}" }.last
|
57
|
+
when :lteq then value.map { |k, v| "#{k}<=#{v}" }.last
|
58
|
+
when :not then value.map { |k, v| "-#{k}:#{v}" }.last
|
59
|
+
else
|
60
|
+
value ? "#{key}=#{value}" : key
|
61
|
+
end
|
62
|
+
end.join(" ")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
Her::Model::Relation.prepend CoreExtensions::HerWithQueryFilter
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Saves the rate limts from Vindi responses.
|
4
|
+
module Vindi
|
5
|
+
module Middleware
|
6
|
+
class RateLimitValidation < Faraday::Middleware
|
7
|
+
def call(env)
|
8
|
+
raise Vindi::RateLimitError, "Rate limit reached" if rate_limit_reached?
|
9
|
+
|
10
|
+
@app.call(env)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def rate_limit_reached?
|
16
|
+
return false unless Vindi::RateLimit.rate_limit_limit
|
17
|
+
|
18
|
+
Vindi::RateLimit.rate_limit_limit <= Vindi::RateLimit.rate_limit_remaining &&
|
19
|
+
Vindi::RateLimit.rate_limit_reset > Time.now
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vindi
|
4
|
+
module Middleware
|
5
|
+
# Vindi Response Parser
|
6
|
+
#
|
7
|
+
# Set metada info to metadata object.
|
8
|
+
class ResponseParser < Faraday::Response::Middleware
|
9
|
+
def on_complete(env)
|
10
|
+
env[:body] = parse(env[:status] == 204 ? "{}" : env)
|
11
|
+
|
12
|
+
Vindi::RateLimit.update env[:body].dig :metadata, :rate_limit
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse(env)
|
16
|
+
json = Her::Middleware::ParseJSON.new.parse_json env[:body]
|
17
|
+
errors = translate_errors_to_activemodel_style(json.delete(:errors) || [])
|
18
|
+
metadata = (json.delete(:metadata) || {}).merge(extract_response_headers_info(env[:response_headers]))
|
19
|
+
|
20
|
+
{
|
21
|
+
data: json,
|
22
|
+
errors: errors,
|
23
|
+
metadata: metadata
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def translate_errors_to_activemodel_style(errors)
|
28
|
+
errors.map do |e|
|
29
|
+
{
|
30
|
+
attribute: e[:parameter],
|
31
|
+
type: e[:id] == "invalid_parameter" ? :invalid : e[:id],
|
32
|
+
message: e[:message]
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def extract_response_headers_info(response_headers)
|
38
|
+
{
|
39
|
+
items: response_headers[:total],
|
40
|
+
link: response_headers[:link],
|
41
|
+
rate_limit: {
|
42
|
+
limit: response_headers[:"rate-limit-limit"],
|
43
|
+
reset: Time.at(response_headers[:"rate-limit-reset"].to_i),
|
44
|
+
remaining: response_headers[:"rate-limit-remaining"]
|
45
|
+
}
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vindi
|
4
|
+
# Bill
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
#
|
8
|
+
# bill = Vindi::Bill.find(1)
|
9
|
+
# bill.charges
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# subscription = Vindi::Subscription.find(1)
|
14
|
+
# bills = subscription.bills
|
15
|
+
#
|
16
|
+
class Bill < Model
|
17
|
+
belongs_to :customer
|
18
|
+
belongs_to :period
|
19
|
+
belongs_to :subscription
|
20
|
+
belongs_to :payment_profile
|
21
|
+
# belongs_to :payment_condition
|
22
|
+
|
23
|
+
has_many :bill_items
|
24
|
+
has_many :charges
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vindi
|
4
|
+
# Bill Item
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
#
|
8
|
+
# bill = Vindi::Bill.find(1)
|
9
|
+
# bill_items = bill.items
|
10
|
+
#
|
11
|
+
class BillItem < Model
|
12
|
+
belongs_to :product
|
13
|
+
belongs_to :product_item
|
14
|
+
belongs_to :discount
|
15
|
+
|
16
|
+
# has_many :usages
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "subscription"
|
4
|
+
|
5
|
+
module Vindi
|
6
|
+
# Customers
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
#
|
10
|
+
# customer = Vindi::Customer.find(1)
|
11
|
+
# customer.subscriptions
|
12
|
+
# customer.subscriptions.active
|
13
|
+
#
|
14
|
+
# customer = Vindi::Customer.find_by(email: "gandalf@middleearth.com")
|
15
|
+
# customer.name
|
16
|
+
# customer.name = "Gandalf the White"
|
17
|
+
# customer.save
|
18
|
+
#
|
19
|
+
class Customer < Model
|
20
|
+
scope :contains_name, ->(name) { where(contains: { name: name }) }
|
21
|
+
|
22
|
+
has_one :address, class_name: "Vindi::Address"
|
23
|
+
# has_many :subscriptions, class_name: "Vindi::Subscription", parent_as_param: true, group_params_with: :query
|
24
|
+
|
25
|
+
def subscriptions
|
26
|
+
Vindi::Subscription.where(customer_id: id)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Vindi
|
4
|
+
# Base model to vindi resources.
|
5
|
+
class Model
|
6
|
+
include Her::Model
|
7
|
+
|
8
|
+
# @example Active subscriptions
|
9
|
+
# @subscriptions = Vindi::Subscription.active
|
10
|
+
#
|
11
|
+
scope :active, -> { where(status: :active) }
|
12
|
+
scope :inactive, -> { where(status: :inactive) }
|
13
|
+
scope :archived, -> { where(status: :archived) }
|
14
|
+
scope :canceled, -> { where(status: :canceled) }
|
15
|
+
|
16
|
+
# @example Active Customers paginated
|
17
|
+
#
|
18
|
+
# @customers = Vindi::Customer.active.per_page(5).page(2)
|
19
|
+
#
|
20
|
+
scope :page, ->(page) { where(page: page) }
|
21
|
+
scope :per_page, ->(per_page) { where(per_page: per_page) }
|
22
|
+
|
23
|
+
# @example Active Customers ordered by name
|
24
|
+
# @customers = Vindi::Customer.active.order_by(:name, :asc)
|
25
|
+
#
|
26
|
+
scope :order_by, ->(attr_name, order = :desc) { where(sort_by: attr_name, sort_order: order) }
|
27
|
+
|
28
|
+
parse_root_in_json true, format: :active_model_serializers
|
29
|
+
|
30
|
+
store_metadata :_metadata
|
31
|
+
|
32
|
+
before_validation
|
33
|
+
|
34
|
+
# Validate record before save and after the request
|
35
|
+
# put the errors in the right places.
|
36
|
+
#
|
37
|
+
# @example A subscription without a customer
|
38
|
+
#
|
39
|
+
# @subscription = Vindi::Subscription.new.tap do |s|
|
40
|
+
# s.plan_id = plan.id
|
41
|
+
# s.payment_method_code = "credit_card"
|
42
|
+
# s.save
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# @subscription.errors.full_messages # ["Customer can't be blank"]
|
46
|
+
#
|
47
|
+
# @example A subscription with invalid plan
|
48
|
+
#
|
49
|
+
# @subscription = Vindi::Subscription.new.tap do |s|
|
50
|
+
# s.customer_id = customer.id
|
51
|
+
# s.plan_id = 1
|
52
|
+
# s.payment_method_code = "credit_card"
|
53
|
+
# s.save
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# @subscription.errors.full_messages # ["Plan nao encontrado"]
|
57
|
+
#
|
58
|
+
def save
|
59
|
+
super
|
60
|
+
|
61
|
+
response_errors.any? && errors.clear && response_errors.each do |re|
|
62
|
+
errors.add re[:attribute], re[:type], message: re[:message]
|
63
|
+
end
|
64
|
+
|
65
|
+
return false if errors.any?
|
66
|
+
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
# @private
|
71
|
+
def valid?
|
72
|
+
super && response_errors.empty?
|
73
|
+
end
|
74
|
+
|
75
|
+
class << self
|
76
|
+
# @example First Customer
|
77
|
+
# @customer = Vindi::Customer.first
|
78
|
+
#
|
79
|
+
# @example First two customers
|
80
|
+
# @customers = Vindi::Customer.first(2)
|
81
|
+
#
|
82
|
+
def first(limit = 1)
|
83
|
+
records = order_by(:created_at, :asc).per_page(limit)
|
84
|
+
|
85
|
+
return records[0] if limit == 1
|
86
|
+
|
87
|
+
records
|
88
|
+
end
|
89
|
+
|
90
|
+
# @example Last customer
|
91
|
+
# @customer = Vindi::Customer.last
|
92
|
+
#
|
93
|
+
# @example Last two customers
|
94
|
+
# @customers = Vindi::Customer.last(2)
|
95
|
+
#
|
96
|
+
def last(limit = 1)
|
97
|
+
records = order_by(:created_at, :desc).per_page(limit)
|
98
|
+
|
99
|
+
return records[0] if limit == 1
|
100
|
+
|
101
|
+
records
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|