rvindi 0.0.3

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/gem-push.yml +45 -0
  3. data/.gitignore +9 -0
  4. data/.rubocop.yml +13 -0
  5. data/.tool-versions +1 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CODE_OF_CONDUCT.md +84 -0
  8. data/Gemfile +21 -0
  9. data/Gemfile.lock +134 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +205 -0
  12. data/Rakefile +8 -0
  13. data/bin/console +22 -0
  14. data/bin/setup +8 -0
  15. data/lib/rvindi.rb +3 -0
  16. data/lib/vindi/core_extensions/her_save_only_changed_attrs.rb +73 -0
  17. data/lib/vindi/core_extensions/her_with_query_filter.rb +69 -0
  18. data/lib/vindi/middleware/rate_limit_validation.rb +23 -0
  19. data/lib/vindi/middleware/response_parser.rb +50 -0
  20. data/lib/vindi/models/address.rb +9 -0
  21. data/lib/vindi/models/bill.rb +26 -0
  22. data/lib/vindi/models/bill_item.rb +18 -0
  23. data/lib/vindi/models/charge.rb +9 -0
  24. data/lib/vindi/models/customer.rb +29 -0
  25. data/lib/vindi/models/discount.rb +8 -0
  26. data/lib/vindi/models/issue.rb +8 -0
  27. data/lib/vindi/models/model.rb +77 -0
  28. data/lib/vindi/models/notification.rb +8 -0
  29. data/lib/vindi/models/payment_method.rb +8 -0
  30. data/lib/vindi/models/payment_profile.rb +9 -0
  31. data/lib/vindi/models/period.rb +8 -0
  32. data/lib/vindi/models/plan.rb +90 -0
  33. data/lib/vindi/models/plan_item.rb +11 -0
  34. data/lib/vindi/models/pricing_schema.rb +8 -0
  35. data/lib/vindi/models/product.rb +25 -0
  36. data/lib/vindi/models/product_item.rb +8 -0
  37. data/lib/vindi/models/subscription.rb +48 -0
  38. data/lib/vindi/models/transaction.rb +8 -0
  39. data/lib/vindi/rate_limit.rb +18 -0
  40. data/lib/vindi/version.rb +5 -0
  41. data/lib/vindi.rb +80 -0
  42. data/rvindi.gemspec +30 -0
  43. metadata +141 -0
@@ -0,0 +1,69 @@
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
+ r.params = r.params.merge(params)
23
+
24
+ # Default params, as order and page number, will always be used.
25
+ default_params = extract_default_params r.params
26
+
27
+ # Query filters is joined into a single param called :query.
28
+ query = [r.params.delete(:query), params_to_query(r.params)].compact.join " "
29
+
30
+ r.params = { query: query }.merge(default_params)
31
+
32
+ r.clear_fetch_cache!
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def extract_default_params(params)
39
+ page = params.delete(:page) || 1
40
+ per_page = params.delete(:per_page) || 25
41
+ sort_by = params.delete(:sort_by) || :created_at
42
+ sort_order = params.delete(:sort_order) || :desc
43
+
44
+ {
45
+ page: page,
46
+ per_page: per_page,
47
+ sort_by: sort_by,
48
+ sort_order: sort_order
49
+ }.delete_if { |_, v| v.nil? }
50
+ end
51
+
52
+ def params_to_query(params)
53
+ params.map do |key, value|
54
+ case key
55
+ when :contains then value.map { |k, v| "#{k}:#{v}" }.last
56
+ when :gt then value.map { |k, v| "#{k}>#{v}" }.last
57
+ when :gteq then value.map { |k, v| "#{k}>=#{v}" }.last
58
+ when :lt then value.map { |k, v| "#{k}<#{v}" }.last
59
+ when :lteq then value.map { |k, v| "#{k}<=#{v}" }.last
60
+ when :not then value.map { |k, v| "-#{k}:#{v}" }.last
61
+ else
62
+ value ? "#{key}=#{value}" : key
63
+ end
64
+ end.join(" ")
65
+ end
66
+ end
67
+ end
68
+
69
+ Her::Model::Relation.prepend CoreExtensions::HerWithQueryFilter
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ module Middleware
5
+ # Saves the rate limts from Vindi responses.
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
+ # Saves metadata info into 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,77 @@
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 :canceled, -> { where(status: :canceled) }
14
+ scope :archived, -> { where(status: :archived) }
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
+ # Archive a record.
33
+ #
34
+ # @example Archive a customer
35
+ #
36
+ # Vindi::Customer.find_by(email: "sarumanthewhite@middlearth.io").archive!
37
+ #
38
+ def archive!
39
+ destroy
40
+ end
41
+
42
+ # @private
43
+ def valid?
44
+ super && response_errors.empty?
45
+ end
46
+
47
+ class << self
48
+ # @example First Customer
49
+ # @customer = Vindi::Customer.first
50
+ #
51
+ # @example First two customers
52
+ # @customers = Vindi::Customer.first(2)
53
+ #
54
+ def first(limit = 1)
55
+ records = order_by(:created_at, :asc).per_page(limit)
56
+
57
+ return records[0] if limit == 1
58
+
59
+ records
60
+ end
61
+
62
+ # @example Last customer
63
+ # @customer = Vindi::Customer.last
64
+ #
65
+ # @example Last two customers
66
+ # @customers = Vindi::Customer.last(2)
67
+ #
68
+ def last(limit = 1)
69
+ records = order_by(:created_at, :desc).per_page(limit)
70
+
71
+ return records[0] if limit == 1
72
+
73
+ records
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Notifications
5
+ #
6
+ class Notitication < Model
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Payment Methods
5
+ #
6
+ class PaymentMethod < Model
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Payment Profiles
5
+ #
6
+ class PaymentProfile < Model
7
+ belongs_to :customer
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Periods
5
+ #
6
+ class Period < Model
7
+ end
8
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Plans
5
+ #
6
+ # @example List active plans
7
+ #
8
+ # plans = Vindi::Plan.active
9
+ #
10
+ # @example Create a recurring plan
11
+ #
12
+ # plan = Vindi::Plan.new.tap do |p|
13
+ # p.name = "Monthly Plan"
14
+ # p.description = "This plan will be renewed every month in the same day"
15
+ # p.period = "monthly"
16
+ # p.recurring = true
17
+ # p.code = 1
18
+ # p.plan_items = [
19
+ # {
20
+ # cycles: nil,
21
+ # product_id: 1
22
+ # }
23
+ # ]
24
+ # end
25
+ #
26
+ # @example Create an yearly plan with installments
27
+ #
28
+ # plan = Vindi::Plan.new.tap do |p|
29
+ # p.name = "Yearly Plan"
30
+ # p.description = "This plan will be paid in 12 installments"
31
+ # p.period = "yearly"
32
+ # p.billing_cycles = 1
33
+ # p.installments = 12
34
+ # p.code = 1
35
+ # p.plan_items = [
36
+ # {
37
+ # cycles: nil,
38
+ # product_id: 1
39
+ # }
40
+ # ]
41
+ # end
42
+ #
43
+ class Plan < Model
44
+ # has_many :plan_items
45
+ # has_many :products, through: :plan_items
46
+
47
+ scope :recurring, -> { where(billing_cycles: nil) }
48
+
49
+ after_initialize :set_defaults
50
+
51
+ def recurring=(value)
52
+ self.billing_cycles = value ? nil : 0
53
+ end
54
+
55
+ def period=(value)
56
+ raise "invalid period" unless %w[monthly quarterly biannually yearly].include? value.to_s
57
+
58
+ send "set_#{value}"
59
+ end
60
+
61
+ private
62
+
63
+ def set_defaults
64
+ self.billing_trigger_type = "beginning_of_period"
65
+ self.billing_trigger_day = 0
66
+ self.installments = 1
67
+ self.status = "active"
68
+ end
69
+
70
+ def set_monthly
71
+ self.interval = "months"
72
+ self.interval_count = 1
73
+ end
74
+
75
+ def set_quarterly
76
+ self.interval = "months"
77
+ self.interval_count = 3
78
+ end
79
+
80
+ def set_biannually
81
+ self.interval = "months"
82
+ self.interval_count = 6
83
+ end
84
+
85
+ def set_yearly
86
+ self.interval = "months"
87
+ self.interval_count = 12
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Plan Items
5
+ #
6
+ class PlanItem < Model
7
+ belongs_to :plan
8
+
9
+ has_one :product
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Pricing Schema
5
+ #
6
+ class PricingSchema < Model
7
+ end
8
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Products
5
+ #
6
+ # @example Active productts
7
+ #
8
+ # products = Vindi::Product.active
9
+ #
10
+ # @example Create a product
11
+ #
12
+ # palantir = Vindi::Product.new.tap do |p|
13
+ # p.code = "palantir"
14
+ # p.name = "Palantir"
15
+ # p.description = "The Twitch of Istari folk"
16
+ # p.pricing_schema = { price: 42.42 }
17
+ # p.save
18
+ # end
19
+ #
20
+ class Product < Model
21
+ belongs_to :pricing_schema
22
+
23
+ has_many :plans
24
+ end
25
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Product Items
5
+ #
6
+ class ProductItem < Model
7
+ end
8
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Customer Subscriptions
5
+ #
6
+ # @example Subscribe a customer to a plan
7
+ #
8
+ # @subscription = Vindi::Subscription.new.tap do |s|
9
+ # s.customer_id = customer.id
10
+ # s.plan_id = plan.id
11
+ # s.payment_method_code = "credit_card"
12
+ # s.save
13
+ # end
14
+ #
15
+ class Subscription < Model
16
+ belongs_to :customer
17
+ belongs_to :plan
18
+
19
+ attributes :plan_id, :customer_id, :payment_method_code
20
+
21
+ validates :plan_id, :customer_id, :payment_method_code, presence: true
22
+
23
+ scope :inactive, -> { canceled }
24
+
25
+ # @example Cancel a subscription
26
+ #
27
+ # @subscription = Vindi::Customer.find(1).subscriptions.active.last
28
+ # @subscription.cancel!
29
+ #
30
+ def cancel!
31
+ destroy
32
+ end
33
+
34
+ # @example Reactivate a subscription
35
+ #
36
+ # @subscription = Vindi::Customer.find(1).subscriptions.inactive.last
37
+ # @subscription.reactivate!
38
+ #
39
+ def reactivate!
40
+ # REVIEW: There's another way to do this using `custom_post` but the result breaks the normal
41
+ # flow because the API returns the root resource as singular name and HER expects to be a plural.
42
+
43
+ self.class.post_raw(:reactivate, id: id) do |parsed_data, _|
44
+ assign_attributes parsed_data[:data][self.class.collection_path.singularize.to_sym]
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ # Transactitons
5
+ #
6
+ class Transactiton < Model
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ class RateLimitError < StandardError; end
5
+
6
+ # Vindi API calls has a rate limit.
7
+ class RateLimit
8
+ class << self
9
+ attr_accessor :rate_limit_limit, :rate_limit_remaining, :rate_limit_reset
10
+
11
+ def update(rate = {})
12
+ @rate_limit_limit = rate[:limit].to_i
13
+ @rate_limit_remaining = rate[:remaining].to_i
14
+ @rate_limit_reset = Time.at(rate[:reset].to_i)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vindi
4
+ VERSION = "0.0.3"
5
+ end
data/lib/vindi.rb ADDED
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-configurable"
4
+
5
+ require "faraday"
6
+ require "faraday_middleware"
7
+ require "her"
8
+
9
+ require_relative "vindi/version"
10
+ require_relative "vindi/rate_limit"
11
+
12
+ require_relative "vindi/middleware/rate_limit_validation"
13
+ require_relative "vindi/middleware/response_parser"
14
+
15
+ require_relative "vindi/core_extensions/her_with_query_filter"
16
+ require_relative "vindi/core_extensions/her_save_only_changed_attrs"
17
+
18
+ module Vindi # :nodoc:
19
+ extend Dry::Configurable
20
+
21
+ class Error < StandardError; end
22
+
23
+ RESOURCE_MODELS = Dir[File.expand_path("vindi/models/**/*.rb", File.dirname(__FILE__))].freeze
24
+
25
+ RESOURCE_MODELS.each do |f|
26
+ autoload File.basename(f, ".rb").camelcase.to_sym, f
27
+ end
28
+
29
+ VINDI_API_URL = "https://app.vindi.com.br/api/v1"
30
+ VINDI_SANDBOX_API_URL = "https://sandbox-app.vindi.com.br/api/v1"
31
+
32
+ # Set sandbox to true in dev mode.
33
+ setting :sandbox, false
34
+ # Set the API KEY to assign the API calls.
35
+ setting :api_key
36
+ # Validates incoming Vindi Webhook calls with the given secret name.
37
+ setting :webhook_secret_name
38
+ # Validates incoming Vindi Webhook calls with the given secret password.
39
+ setting :webhook_secret_password
40
+
41
+ # @example
42
+ # Vindi.configure do |config|
43
+ # config.sandbox = true
44
+ # config.api_key = "MY API KEY"
45
+ # config.webhook_secret_name = "A SECRET NAMEE"
46
+ # config.webhook_secret_password = "A SECRET PASSWORD"
47
+ # end
48
+ #
49
+ def self.configure
50
+ super
51
+
52
+ her_setup
53
+ end
54
+
55
+ def self.api_url
56
+ return VINDI_SANDBOX_API_URL if config.sandbox
57
+
58
+ VINDI_API_URL
59
+ end
60
+
61
+ # @private
62
+ def self.her_setup
63
+ Her::API.setup url: Vindi.api_url do |conn|
64
+ conn.headers["User-Agent"] = "wedsonlima/rvindi #{Vindi::VERSION}"
65
+
66
+ conn.basic_auth config.api_key, ""
67
+
68
+ # Request
69
+ conn.use ::Vindi::Middleware::RateLimitValidation
70
+ conn.request :json
71
+
72
+ # Response
73
+ conn.response :json, content_type: /\bjson$/
74
+ conn.use ::Vindi::Middleware::ResponseParser
75
+
76
+ # Adapter
77
+ conn.adapter Faraday.default_adapter
78
+ end
79
+ end
80
+ end