conexa 0.1.1 → 0.2.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 +4 -4
- data/CHANGELOG.md +265 -1
- data/README.md +36 -4
- data/README_pt-BR.md +37 -3
- data/REFERENCE.md +53 -10
- data/lib/conexa/configuration.rb +48 -2
- data/lib/conexa/deprecation.rb +44 -0
- data/lib/conexa/errors.rb +79 -7
- data/lib/conexa/model.rb +80 -14
- data/lib/conexa/object.rb +24 -3
- data/lib/conexa/request.rb +90 -11
- data/lib/conexa/resources/charge.rb +61 -8
- data/lib/conexa/resources/company.rb +20 -1
- data/lib/conexa/resources/contract.rb +147 -30
- data/lib/conexa/resources/credit_card.rb +46 -0
- data/lib/conexa/resources/recurring_sale.rb +16 -5
- data/lib/conexa/resources/result.rb +23 -5
- data/lib/conexa/util.rb +49 -9
- data/lib/conexa/version.rb +1 -1
- data/lib/conexa.rb +40 -3
- metadata +28 -70
- data/.editorconfig +0 -30
- data/.rspec +0 -3
- data/.rubocop.yml +0 -13
- data/.solargraph.yml +0 -2
- data/.vscode/launch.json +0 -30
- data/Gemfile +0 -18
- data/Gemfile.lock +0 -159
- data/Rakefile +0 -12
- data/bin/console +0 -61
- data/bin/setup +0 -8
- data/docs/postman-collection.json +0 -30785
- data/lib/conexa/authenticator.rb +0 -116
- data/lib/conexa/order_common.rb +0 -44
- data/lib/conexa/token_manager.rb +0 -136
- data/scripts/extract_fixtures.rb +0 -35
data/lib/conexa/errors.rb
CHANGED
|
@@ -16,40 +16,112 @@ module Conexa
|
|
|
16
16
|
class RequestError < ConexaError
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
# Raised instead of performing a mutating request while Conexa.read_only?.
|
|
20
|
+
# The request never reaches the network.
|
|
21
|
+
class ReadOnlyError < ConexaError
|
|
22
|
+
end
|
|
23
|
+
|
|
19
24
|
class ResponseError < ConexaError
|
|
20
25
|
attr_reader :request_params, :error
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
# The decoded error body, when the API returned one.
|
|
28
|
+
# @return [Hash]
|
|
29
|
+
attr_reader :api_response
|
|
30
|
+
|
|
31
|
+
def initialize(request_params, error, message=nil, api_response=nil)
|
|
23
32
|
@request_params, @error = request_params, error
|
|
24
|
-
|
|
33
|
+
@api_response = api_response.is_a?(Hash) ? api_response : {}
|
|
34
|
+
msg = describe_error(error)
|
|
25
35
|
msg += " => " + message if message
|
|
26
36
|
super msg
|
|
27
37
|
end
|
|
38
|
+
|
|
39
|
+
# The API's errors, normalised across its two shapes.
|
|
40
|
+
#
|
|
41
|
+
# Field validation answers `{"field": …, "messages": [...]}`; business rules
|
|
42
|
+
# answer `{"code": …, "message": …}`. Consumers that only handled the first
|
|
43
|
+
# rendered business-rule errors as blank strings — which is how
|
|
44
|
+
# CONTRACT_RECURRING_SALE_10 stayed invisible through eight attempts.
|
|
45
|
+
#
|
|
46
|
+
# @return [Array<Hash{Symbol=>String,nil}>] entries of {field:, code:, message:}
|
|
47
|
+
def api_errors
|
|
48
|
+
Array(api_response["errors"]).filter_map do |entry|
|
|
49
|
+
next unless entry.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
{ field: entry["field"],
|
|
52
|
+
code: entry["code"],
|
|
53
|
+
message: entry["message"] || Array(entry["messages"]).join("; ") }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Documented business-rule codes, e.g. "CHARGE_11" or
|
|
58
|
+
# "CONTRACT_RECURRING_SALE_10". These are what a caller branches on — for
|
|
59
|
+
# instance to tell an already-settled charge from a real settlement failure.
|
|
60
|
+
#
|
|
61
|
+
# @return [Array<String>]
|
|
62
|
+
def api_error_codes
|
|
63
|
+
api_errors.filter_map { |entry| entry[:code] }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# One readable line per error, whichever shape it arrived in.
|
|
67
|
+
# @return [Array<String>]
|
|
68
|
+
def api_error_messages
|
|
69
|
+
api_errors.map do |entry|
|
|
70
|
+
label = entry[:field] || entry[:code]
|
|
71
|
+
label ? "#{label}: #{entry[:message]}" : entry[:message]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# `error` is usually a RestClient::Exception, but the malformed-body path
|
|
78
|
+
# hands us a RestClient::Response, which has no #message — that used to raise
|
|
79
|
+
# NoMethodError from inside the error constructor itself.
|
|
80
|
+
def describe_error(error)
|
|
81
|
+
return error.message if error.respond_to?(:message) && error.message
|
|
82
|
+
return "HTTP #{error.code}: #{error.body.to_s[0, 200]}" if error.respond_to?(:code)
|
|
83
|
+
|
|
84
|
+
error.to_s
|
|
85
|
+
end
|
|
28
86
|
end
|
|
29
87
|
|
|
30
88
|
class NotFound < ResponseError
|
|
31
89
|
attr_reader :response
|
|
32
90
|
def initialize(response, request_params, error)
|
|
33
91
|
@response = response
|
|
34
|
-
super request_params, error, response&.dig('message')
|
|
92
|
+
super request_params, error, response&.dig('message'), response
|
|
35
93
|
end
|
|
36
94
|
end
|
|
37
95
|
|
|
96
|
+
# Raised for an error body with no `message` key.
|
|
97
|
+
#
|
|
98
|
+
# Every error response the published collection documents carries a `message`,
|
|
99
|
+
# so in practice this is reached only by an undocumented or malformed body. It
|
|
100
|
+
# used to render as the bare string "Conexa::ValidationError" and `#to_h` raised
|
|
101
|
+
# NoMethodError; both now degrade to something a caller can act on.
|
|
38
102
|
class ValidationError < ConexaError
|
|
39
103
|
attr_reader :response, :errors
|
|
40
104
|
|
|
41
105
|
def initialize(response)
|
|
42
106
|
@response = response
|
|
43
|
-
@errors = response['message']
|
|
44
|
-
|
|
45
|
-
|
|
107
|
+
@errors = Array(response.is_a?(Hash) ? response['message'] : nil).filter_map do |msg|
|
|
108
|
+
next unless msg.is_a?(Hash)
|
|
109
|
+
|
|
110
|
+
ParamError.new(*msg.values_at('message', 'parameter_name', 'type', 'url'))
|
|
46
111
|
end
|
|
47
|
-
|
|
112
|
+
|
|
113
|
+
super(@errors.any? ? @errors.map(&:message).join(', ') : describe(response))
|
|
48
114
|
end
|
|
49
115
|
|
|
50
116
|
def to_h
|
|
51
117
|
@errors.map(&:to_h)
|
|
52
118
|
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def describe(response)
|
|
123
|
+
"The API returned an error with no message: #{response.inspect[0, 200]}"
|
|
124
|
+
end
|
|
53
125
|
end
|
|
54
126
|
|
|
55
127
|
class MissingCredentialsError < ConexaError
|
data/lib/conexa/model.rb
CHANGED
|
@@ -15,16 +15,18 @@ module Conexa
|
|
|
15
15
|
#
|
|
16
16
|
# == Primary Key
|
|
17
17
|
#
|
|
18
|
-
# Each resource
|
|
19
|
-
# that
|
|
18
|
+
# Each resource declares primary_key_attribute, which defines #id and the
|
|
19
|
+
# operations that need the resource ID (destroy, save, fetch, etc.):
|
|
20
20
|
#
|
|
21
21
|
# class Charge < Model
|
|
22
22
|
# primary_key_attribute :charge_id
|
|
23
23
|
# end
|
|
24
24
|
#
|
|
25
|
-
# charge.id # => 123 (alias for charge_id)
|
|
26
25
|
# charge.charge_id # => 123
|
|
27
26
|
# charge.chargeId # => 123 (camelCase alias for backwards compat)
|
|
27
|
+
# charge.id # => 123 — the resource's own key, falling back to a
|
|
28
|
+
# # plain "id" attribute, which is what write endpoints
|
|
29
|
+
# # return and what Model#create reads back
|
|
28
30
|
#
|
|
29
31
|
# == Why explicit primary_key_attribute?
|
|
30
32
|
#
|
|
@@ -33,18 +35,41 @@ module Conexa
|
|
|
33
35
|
# instead of "recurring_sale_id". Explicit declaration ensures correctness.
|
|
34
36
|
#
|
|
35
37
|
class Model < ConexaObject
|
|
38
|
+
extend Deprecatable
|
|
39
|
+
|
|
36
40
|
def create
|
|
37
|
-
|
|
41
|
+
created = Conexa::Request.post(self.class.show_url, params: to_hash).call(class_name)
|
|
42
|
+
|
|
43
|
+
# A create that answers with no usable body leaves us nothing to identify
|
|
44
|
+
# the new record by, so there is nothing to re-fetch. Returning the local
|
|
45
|
+
# object is honest; raising NoMethodError from `nil.attributes` was not.
|
|
46
|
+
return self unless created.respond_to?(:attributes)
|
|
47
|
+
|
|
48
|
+
set_primary_key created.attributes['id']
|
|
38
49
|
fetch
|
|
39
50
|
end
|
|
40
51
|
|
|
41
52
|
def save
|
|
53
|
+
# #destroy has always guarded this; #save did not, so an object with no id
|
|
54
|
+
# silently issued `PATCH /customer/` instead of failing fast.
|
|
55
|
+
raise RequestError.new('Invalid ID') unless id.present?
|
|
56
|
+
|
|
42
57
|
update Conexa::Request.patch(self.class.show_url(primary_key), params: unsaved_attributes).call(class_name)
|
|
43
58
|
self
|
|
44
59
|
end
|
|
45
60
|
|
|
46
61
|
def fetch
|
|
47
|
-
|
|
62
|
+
fetched = self.class.find(primary_key)
|
|
63
|
+
|
|
64
|
+
# #update ignores anything with no attributes, which is right for a write
|
|
65
|
+
# that answers with no body — but a *refresh* that comes back empty must
|
|
66
|
+
# not quietly leave stale values in place reporting success.
|
|
67
|
+
unless fetched.respond_to?(:attributes)
|
|
68
|
+
raise ResponseError.new({ url: self.class.show_url(primary_key) }, nil,
|
|
69
|
+
"a API respondeu sem corpo: nada para atualizar")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
update fetched
|
|
48
73
|
self
|
|
49
74
|
end
|
|
50
75
|
|
|
@@ -75,10 +100,10 @@ module Conexa
|
|
|
75
100
|
end
|
|
76
101
|
|
|
77
102
|
class << self
|
|
78
|
-
# DSL for primary key attribute
|
|
103
|
+
# DSL for the primary key attribute
|
|
79
104
|
# @example
|
|
80
105
|
# primary_key_attribute :charge_id
|
|
81
|
-
# # Generates: charge_id
|
|
106
|
+
# # Generates: charge_id + chargeId alias + #id (with an "id" fallback)
|
|
82
107
|
def primary_key_attribute(snake_name)
|
|
83
108
|
camel_name = Util.camelize_str(snake_name.to_s)
|
|
84
109
|
|
|
@@ -87,7 +112,14 @@ module Conexa
|
|
|
87
112
|
end
|
|
88
113
|
|
|
89
114
|
alias_method camel_name.to_sym, snake_name
|
|
90
|
-
|
|
115
|
+
|
|
116
|
+
# Not an alias: #id has to keep Model#id's documented fallback to a plain
|
|
117
|
+
# "id" attribute. Write endpoints answer with {"id": N} rather than the
|
|
118
|
+
# resource's own key — Model#create depends on exactly that — so aliasing
|
|
119
|
+
# #id straight to #charge_id silently made the fallback dead code.
|
|
120
|
+
define_method(:id) do
|
|
121
|
+
@attributes[snake_name.to_s] || @attributes["id"]
|
|
122
|
+
end
|
|
91
123
|
end
|
|
92
124
|
|
|
93
125
|
def create(*args)
|
|
@@ -95,19 +127,32 @@ module Conexa
|
|
|
95
127
|
end
|
|
96
128
|
|
|
97
129
|
def find_by_id(id, **options)
|
|
130
|
+
# Surrounding whitespace is a copy-paste artefact, not a different id —
|
|
131
|
+
# strip it rather than failing. Anything still unusable in a URL is caught
|
|
132
|
+
# by Request#full_api_url and raised as a RequestError.
|
|
133
|
+
id = id.to_s.strip if id.is_a?(String)
|
|
98
134
|
raise RequestError.new('Invalid ID') unless id.present?
|
|
135
|
+
|
|
99
136
|
Conexa::Request.get(show_url(id), params: options).call underscored_class_name
|
|
100
137
|
end
|
|
101
138
|
alias :find :find_by_id
|
|
102
139
|
|
|
103
140
|
def find_by(params = Hash.new, page = nil, size = nil)
|
|
141
|
+
# extract_page_size_or_params always returns limit/offset now, and
|
|
142
|
+
# validates them, so there is no page/size left here to guard.
|
|
104
143
|
params = extract_page_size_or_params(page, size, **params)
|
|
105
|
-
raise RequestError.new('Invalid page size') if (!params.key?(:limit)) && (params[:page] < 1 or params[:size] < 1)
|
|
106
144
|
|
|
107
|
-
Conexa::Request.get(url, params: params).call(
|
|
145
|
+
result = Conexa::Request.get(url, params: params).call(
|
|
108
146
|
underscored_class_name,
|
|
109
147
|
query_context: { resource_class: self, params: params }
|
|
110
148
|
)
|
|
149
|
+
|
|
150
|
+
# A listing always answers with a Result, as the READMEs promise. Without
|
|
151
|
+
# this, an empty body yielded nil and a bare-array body yielded an Array,
|
|
152
|
+
# so `.data` / `.pagination` / `.next_page` blew up far from the cause.
|
|
153
|
+
return result if result.is_a?(Conexa::Result)
|
|
154
|
+
|
|
155
|
+
Conexa::Result.new("data" => Array(result), "pagination" => nil)
|
|
111
156
|
end
|
|
112
157
|
alias :find_by_hash :find_by
|
|
113
158
|
|
|
@@ -166,11 +211,32 @@ module Conexa
|
|
|
166
211
|
return params
|
|
167
212
|
end
|
|
168
213
|
|
|
169
|
-
#
|
|
214
|
+
# Legacy pagination (page/size) — deprecated, and broken upstream.
|
|
215
|
+
#
|
|
216
|
+
# The API validates `page` and then ignores it, always returning the
|
|
217
|
+
# first page with offset 0 and hasNext true, so a loop over `page` never
|
|
218
|
+
# terminates and silently re-yields the same batch. Converting to
|
|
219
|
+
# limit/offset fixes existing callers instead of leaving them with
|
|
220
|
+
# plausible wrong answers.
|
|
170
221
|
if params.key?(:page) || params.key?(:size) || page_val.is_a?(Integer)
|
|
171
|
-
|
|
172
|
-
params
|
|
173
|
-
|
|
222
|
+
page = params.delete(:page) || page_val || 1
|
|
223
|
+
size = params.delete(:size) || size_val || 100
|
|
224
|
+
|
|
225
|
+
unless page.is_a?(Integer) && page.positive?
|
|
226
|
+
raise RequestError, "page must be a positive integer"
|
|
227
|
+
end
|
|
228
|
+
unless size.is_a?(Integer) && size.positive?
|
|
229
|
+
raise RequestError, "size must be a positive integer"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
deprecate(:page_size,
|
|
233
|
+
"page/size foi substituído por limit/offset e será removido em " \
|
|
234
|
+
"conexa 0.3.0. A API v2 valida `page` e depois o ignora, devolvendo " \
|
|
235
|
+
"sempre a primeira página; os valores são convertidos para " \
|
|
236
|
+
"limit=size, offset=(page-1)*size.")
|
|
237
|
+
|
|
238
|
+
params[:limit] = size
|
|
239
|
+
params[:offset] = (page - 1) * size
|
|
174
240
|
return params
|
|
175
241
|
end
|
|
176
242
|
|
data/lib/conexa/object.rb
CHANGED
|
@@ -65,19 +65,40 @@ module Conexa
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
protected
|
|
68
|
+
# Merge a response into this object.
|
|
69
|
+
#
|
|
70
|
+
# Anything that carries no attributes is a no-op rather than an error. A
|
|
71
|
+
# write may answer with no body (Request#call then yields nil), with `{}`, or
|
|
72
|
+
# — for the top-level-array shape Request#run explicitly handles — with an
|
|
73
|
+
# Array or a scalar, which ConexaObject.convert passes straight through.
|
|
74
|
+
# None of those can update an object, and none of them should raise a bare
|
|
75
|
+
# NoMethodError at the caller: that is the failure this release exists to
|
|
76
|
+
# remove.
|
|
77
|
+
#
|
|
78
|
+
# The empty case matters beyond the crash. `removed_attributes` deletes every
|
|
79
|
+
# key absent from the incoming hash, which is right for a full refresh and
|
|
80
|
+
# destructive for a write that answers `{}` — that used to wipe the object,
|
|
81
|
+
# primary key included, and report success.
|
|
68
82
|
def update(attributes)
|
|
69
|
-
|
|
83
|
+
return self unless attributes.respond_to?(:to_hash)
|
|
84
|
+
|
|
85
|
+
incoming = attributes.to_hash
|
|
86
|
+
return self if incoming.empty?
|
|
87
|
+
|
|
88
|
+
removed_attributes = @attributes.keys - incoming.keys
|
|
70
89
|
|
|
71
90
|
removed_attributes.each do |key|
|
|
72
91
|
@attributes.delete key
|
|
73
92
|
end
|
|
74
93
|
|
|
75
|
-
|
|
94
|
+
incoming.each do |key, value|
|
|
76
95
|
key = Util.to_snake_case(key.to_s)
|
|
77
96
|
|
|
78
97
|
@attributes[key] = ConexaObject.convert(value, Util.singularize(key))
|
|
79
98
|
@unsaved_attributes.delete key
|
|
80
99
|
end
|
|
100
|
+
|
|
101
|
+
self
|
|
81
102
|
end
|
|
82
103
|
|
|
83
104
|
def to_hash_value(value, type)
|
|
@@ -120,7 +141,7 @@ module Conexa
|
|
|
120
141
|
end
|
|
121
142
|
|
|
122
143
|
class << self
|
|
123
|
-
def convert(response, resource_name = nil
|
|
144
|
+
def convert(response, resource_name = nil)
|
|
124
145
|
case response
|
|
125
146
|
when Array
|
|
126
147
|
response.map{ |i| convert i, resource_name }
|
data/lib/conexa/request.rb
CHANGED
|
@@ -23,15 +23,57 @@ module Conexa
|
|
|
23
23
|
@auth = options[:auth] || false
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Verbs allowed while Conexa.read_only? — GET, plus authentication, without
|
|
27
|
+
# which read-only mode could not obtain a token in the first place.
|
|
28
|
+
READ_METHODS = %w(GET).freeze
|
|
29
|
+
|
|
30
|
+
# The authentication exemption is tied to these paths, not to the caller's
|
|
31
|
+
# `auth:` flag. `Request.auth` is public, so trusting the flag alone let any
|
|
32
|
+
# write opt out of the guard with `Request.auth("/charge/settle/1", …)`.
|
|
33
|
+
AUTH_PATHS = %w(/auth).freeze
|
|
34
|
+
|
|
26
35
|
def run
|
|
27
|
-
|
|
36
|
+
enforce_read_only!
|
|
28
37
|
|
|
29
|
-
response =
|
|
30
|
-
return {data: response.dig("data") || response, pagination: response.dig("pagination")}
|
|
38
|
+
response = RestClient::Request.execute request_params
|
|
31
39
|
|
|
40
|
+
# A successful write may answer with no body at all: PATCH /charge/settle/:id
|
|
41
|
+
# documents 204 + empty body as its success response, and
|
|
42
|
+
# PATCH /contract/end/:id answers 200 with one. With the Oj adapter,
|
|
43
|
+
# MultiJson.decode("") returns nil *without* raising ParseError, so the
|
|
44
|
+
# nil has to be caught here rather than in a rescue.
|
|
45
|
+
body = response.body.to_s
|
|
46
|
+
return {} if body.strip.empty?
|
|
47
|
+
|
|
48
|
+
decoded = MultiJson.decode(body)
|
|
49
|
+
return {} if decoded.nil?
|
|
50
|
+
|
|
51
|
+
# A top-level array (some list endpoints) has no #dig(String).
|
|
52
|
+
return {data: decoded, pagination: nil} unless decoded.is_a?(Hash)
|
|
53
|
+
|
|
54
|
+
{data: decoded["data"] || decoded, pagination: decoded["pagination"]}
|
|
55
|
+
|
|
56
|
+
# Connection-level failures first. These subclass RestClient::Exception, so
|
|
57
|
+
# listing them after it made them unreachable — Ruby matches rescue clauses
|
|
58
|
+
# top-down. The broad clause then tried to decode their (nil) http_body and
|
|
59
|
+
# raised NoMethodError instead of the documented ConnectionError.
|
|
60
|
+
#
|
|
61
|
+
# All of these carry no response, so there is nothing to classify: they are
|
|
62
|
+
# failures to reach the API, not answers from it. Note that a real HTTP 408
|
|
63
|
+
# is RestClient::RequestTimeout, a *superclass* of Exceptions::Timeout, so
|
|
64
|
+
# it correctly stays in the response taxonomy below.
|
|
65
|
+
rescue SocketError, RestClient::ServerBrokeConnection,
|
|
66
|
+
RestClient::SSLCertificateNotVerified,
|
|
67
|
+
RestClient::Exceptions::Timeout => error
|
|
68
|
+
raise Conexa::ConnectionError.new error
|
|
32
69
|
rescue RestClient::Exception => error
|
|
33
70
|
begin
|
|
34
|
-
|
|
71
|
+
# nil for an error carrying no body; MultiJson.decode(nil) returns nil
|
|
72
|
+
# rather than raising, so the guard has to be here. An error body that
|
|
73
|
+
# decodes to an array or a scalar has no #[](String) either, and used
|
|
74
|
+
# to raise TypeError from inside this handler.
|
|
75
|
+
parsed_error = MultiJson.decode(error.http_body.to_s)
|
|
76
|
+
parsed_error = {} unless parsed_error.is_a?(Hash)
|
|
35
77
|
|
|
36
78
|
if error.is_a? RestClient::ResourceNotFound
|
|
37
79
|
if parsed_error['message']
|
|
@@ -41,7 +83,8 @@ module Conexa
|
|
|
41
83
|
end
|
|
42
84
|
else
|
|
43
85
|
if parsed_error['message']
|
|
44
|
-
raise Conexa::ResponseError.new(request_params, error,
|
|
86
|
+
raise Conexa::ResponseError.new(request_params, error,
|
|
87
|
+
describe_api_error(parsed_error), parsed_error)
|
|
45
88
|
else
|
|
46
89
|
raise Conexa::ValidationError.new parsed_error
|
|
47
90
|
end
|
|
@@ -50,13 +93,40 @@ module Conexa
|
|
|
50
93
|
raise Conexa::ResponseError.new(request_params, error)
|
|
51
94
|
end
|
|
52
95
|
rescue MultiJson::ParseError
|
|
53
|
-
|
|
54
|
-
|
|
96
|
+
# Only genuinely malformed JSON reaches here — empty and null bodies are
|
|
97
|
+
# handled above, for every status.
|
|
55
98
|
raise Conexa::ResponseError.new(request_params, response)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The API's `message` plus its `errors`, rendered as prose.
|
|
102
|
+
#
|
|
103
|
+
# This used to be `message + "=> Erros: " + errors.to_s`, which appended a
|
|
104
|
+
# dangling "=> Erros: " to the 75 documented responses that carry no `errors`
|
|
105
|
+
# array, and dumped Ruby's `#inspect` of an array of hashes for the ones that
|
|
106
|
+
# do. ResponseError#api_error_messages already normalises both shapes.
|
|
107
|
+
def describe_api_error(parsed_error)
|
|
108
|
+
message = parsed_error['message'].to_s
|
|
109
|
+
details = Conexa::ResponseError.new({}, nil, nil, parsed_error).api_error_messages
|
|
110
|
+
return message if details.empty?
|
|
111
|
+
|
|
112
|
+
"#{message} — #{details.join("; ")}"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @raise [Conexa::ReadOnlyError] when a mutating verb is attempted while
|
|
116
|
+
# Conexa.read_only? — checked before the request is executed, so nothing
|
|
117
|
+
# reaches the tenant.
|
|
118
|
+
def enforce_read_only!
|
|
119
|
+
return unless Conexa.read_only?
|
|
120
|
+
return if READ_METHODS.include?(method.to_s.upcase)
|
|
121
|
+
return if @auth && AUTH_PATHS.include?(path)
|
|
122
|
+
|
|
123
|
+
# Deliberately `path`, not `full_api_url`: the latter validates the URL and
|
|
124
|
+
# can raise RequestError, which would win over this one purely because the
|
|
125
|
+
# message is interpolated first. Read-only is a policy — it applies whatever
|
|
126
|
+
# the path looks like.
|
|
127
|
+
raise Conexa::ReadOnlyError,
|
|
128
|
+
"Conexa is in read-only mode: refusing #{method.to_s.upcase} #{path}. " \
|
|
129
|
+
"Unset config.read_only (or CONEXA_READ_ONLY) to allow writes."
|
|
60
130
|
end
|
|
61
131
|
|
|
62
132
|
def call(resource_name, query_context: nil)
|
|
@@ -120,6 +190,15 @@ module Conexa
|
|
|
120
190
|
url += '?' + URI.encode_www_form(query)
|
|
121
191
|
end
|
|
122
192
|
|
|
193
|
+
# An unusable path (a stray space in an id, say) would otherwise surface as
|
|
194
|
+
# URI::InvalidURIError from inside RestClient — outside Conexa::ConexaError,
|
|
195
|
+
# so no caller could rescue it meaningfully.
|
|
196
|
+
begin
|
|
197
|
+
URI.parse(url)
|
|
198
|
+
rescue URI::InvalidURIError
|
|
199
|
+
raise Conexa::RequestError, "Invalid request path: #{path.inspect}"
|
|
200
|
+
end
|
|
201
|
+
|
|
123
202
|
url
|
|
124
203
|
end
|
|
125
204
|
end
|
|
@@ -29,29 +29,82 @@ module Conexa
|
|
|
29
29
|
class Charge < Model
|
|
30
30
|
primary_key_attribute :charge_id
|
|
31
31
|
|
|
32
|
-
#
|
|
32
|
+
# The values `status` can take on a charge, per the collection's field table
|
|
33
|
+
# for `GET /charge/:id`.
|
|
34
|
+
#
|
|
35
|
+
# **`excluded` is here but not in {FILTERABLE_STATUSES}.** The two lists are
|
|
36
|
+
# not the same thing: a charge can hold a status you cannot query by. This
|
|
37
|
+
# constant shipped with nine values in 0.2.1 because it was built from the
|
|
38
|
+
# filter's rejection message rather than from the field table.
|
|
39
|
+
STATUSES = %w[unpaid paid negotiated generatedByNegotiation cancelled
|
|
40
|
+
denied thirdPartyCompany protested juridical excluded].freeze
|
|
41
|
+
|
|
42
|
+
# What `GET /charges?status=` accepts. The API names them in the 400 it
|
|
43
|
+
# returns for an unrecognised value, so this list is the API's own:
|
|
44
|
+
#
|
|
45
|
+
# status=zzz -> 400 "Status is not on the list (unpaid, negotiated,
|
|
46
|
+
# generatedByNegotiation, cancelled, paid, denied,
|
|
47
|
+
# thirdPartyCompany, protested, juridical)"
|
|
48
|
+
#
|
|
49
|
+
# `pending` and `overdue` are in neither list, which is why the predicates
|
|
50
|
+
# built on them never matched.
|
|
51
|
+
FILTERABLE_STATUSES = (STATUSES - %w[excluded]).freeze
|
|
52
|
+
|
|
33
53
|
# @return [Boolean]
|
|
34
54
|
def paid?
|
|
35
55
|
status == 'paid'
|
|
36
56
|
end
|
|
37
57
|
|
|
38
|
-
#
|
|
58
|
+
# Is this charge still open?
|
|
39
59
|
# @return [Boolean]
|
|
40
|
-
def
|
|
41
|
-
status == '
|
|
60
|
+
def unpaid?
|
|
61
|
+
status == 'unpaid'
|
|
42
62
|
end
|
|
43
63
|
|
|
44
|
-
# Check if charge is overdue
|
|
45
64
|
# @return [Boolean]
|
|
65
|
+
def cancelled?
|
|
66
|
+
status == 'cancelled'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @deprecated The API has no `pending` status; the open state is `unpaid`.
|
|
70
|
+
# This alias only exists so callers written against the old, never-matching
|
|
71
|
+
# predicate keep working while they migrate.
|
|
72
|
+
# @return [Boolean]
|
|
73
|
+
def pending?
|
|
74
|
+
self.class.deprecate(:pending?,
|
|
75
|
+
"`Charge#pending?` foi renomeado para `unpaid?` em conexa 0.2.1 — " \
|
|
76
|
+
"a API v2 não tem status `pending`, o estado em aberto chama-se " \
|
|
77
|
+
"`unpaid`. O alias será removido em 0.3.0.")
|
|
78
|
+
unpaid?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @deprecated The API has no `overdue` status. An overdue charge is `unpaid`
|
|
82
|
+
# with a `due_date` in the past; compare the date yourself.
|
|
83
|
+
# @return [Boolean] always false
|
|
46
84
|
def overdue?
|
|
47
|
-
|
|
85
|
+
self.class.deprecate(:overdue?,
|
|
86
|
+
"`Charge#overdue?` sempre devolveu false — a API v2 não tem status " \
|
|
87
|
+
"`overdue`. Use `unpaid?` e compare `due_date`. O método será " \
|
|
88
|
+
"removido em 0.3.0.")
|
|
89
|
+
false
|
|
48
90
|
end
|
|
49
91
|
|
|
50
92
|
# Settle (pay) this charge
|
|
51
|
-
#
|
|
93
|
+
#
|
|
94
|
+
# Moves money and, on a configured tenant, issues an NF-e. Not safe to retry
|
|
95
|
+
# blindly: a second attempt on a settled charge answers 422 CHARGE_11.
|
|
96
|
+
#
|
|
97
|
+
# The API answers 204 with an empty body on success.
|
|
98
|
+
#
|
|
99
|
+
# @param params [Hash] settlement details
|
|
100
|
+
# @option params [String] :settlement_date required, yyyy-MM-dd
|
|
101
|
+
# @option params [Hash] :receiving_method required, {id:, installments_quantity:}
|
|
102
|
+
# @option params [Integer] :account_id required
|
|
103
|
+
# @option params [Float] :paid_amount defaults to the charge amount, without interest
|
|
104
|
+
# @option params [Boolean] :send_email defaults to false
|
|
52
105
|
# @return [self]
|
|
53
106
|
def settle(params = {})
|
|
54
|
-
Conexa::Request.
|
|
107
|
+
Conexa::Request.patch(self.class.show_url("settle", primary_key), params: params).call(class_name)
|
|
55
108
|
self
|
|
56
109
|
end
|
|
57
110
|
|
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Conexa
|
|
4
|
-
|
|
4
|
+
# Company resource (Empresa / unidade)
|
|
5
|
+
#
|
|
6
|
+
# @example List companies
|
|
7
|
+
# Conexa::Company.all(limit: 50)
|
|
8
|
+
#
|
|
9
|
+
# @example Find a company
|
|
10
|
+
# Conexa::Company.find(3)
|
|
11
|
+
class Company < Model
|
|
12
|
+
class << self
|
|
13
|
+
# Model#url pluralizes by appending "s", which yields "/companys" and 404s.
|
|
14
|
+
# Any resource with an irregular English plural has to override this — see
|
|
15
|
+
# spec/contract/api_contract_spec.rb, which checks every resource's URL
|
|
16
|
+
# against the published collection.
|
|
17
|
+
def url(*params)
|
|
18
|
+
["/companies", *params].join '/'
|
|
19
|
+
end
|
|
5
20
|
|
|
21
|
+
def show_url(*params)
|
|
22
|
+
["/company", *params].join '/'
|
|
23
|
+
end
|
|
24
|
+
end
|
|
6
25
|
end
|
|
7
26
|
end
|