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.
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