vindi-hermes 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+
6
+ RuboCop::RakeTask.new
7
+
8
+ task default: :rubocop
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Address
5
+ #
6
+ class Address < Model
7
+ belongs_to :customer
8
+ end
9
+ 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Bill Charges
5
+ #
6
+ class Charge < Model
7
+ belongs_to :bill
8
+ end
9
+ 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Discounts
5
+ #
6
+ class Discount < Model
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Issues
5
+ #
6
+ class Issue < Model
7
+ end
8
+ 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