vindi-hermes 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|