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.
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
- def initialize(request_params, error, message=nil)
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
- msg = @error.message
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']&.map do |msg|
44
- params = msg.values_at('message', 'parameter_name', 'type', 'url')
45
- ParamError.new(*params)
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
- super @errors&.map(&:message)&.join(', ')
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 needs primary_key_attribute for :id alias and operations
19
- # that require the resource ID (destroy, save, fetch, etc.):
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
- set_primary_key Conexa::Request.post(self.class.show_url, params: to_hash).call(class_name).attributes['id']
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
- update self.class.find(primary_key)
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 with :id alias
103
+ # DSL for the primary key attribute
79
104
  # @example
80
105
  # primary_key_attribute :charge_id
81
- # # Generates: charge_id method + chargeId alias + id alias
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
- alias_method :id, snake_name
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
- # Explicit legacy pagination (page/size) — deprecated
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
- warn "DEPRECATION WARNING: O modelo antigo de paginação (page/size) será removido em 01 de agosto de 2026. Utilize limit e offset."
172
- params[:page] ||= page_val || 1
173
- params[:size] ||= size_val || 100
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
- removed_attributes = @attributes.keys - attributes.to_hash.keys
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
- attributes.each do |key, value|
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, client_key=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 }
@@ -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
- response = RestClient::Request.execute request_params
36
+ enforce_read_only!
28
37
 
29
- response = MultiJson.decode response.body
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
- parsed_error = MultiJson.decode error.http_body
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, parsed_error['message'] + "=> Erros: "+ parsed_error['errors'].to_s)
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
- return {} if response.code == 204
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
- rescue SocketError
57
- raise Conexa::ConnectionError.new $!
58
- rescue RestClient::ServerBrokeConnection
59
- raise Conexa::ConnectionError.new $!
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
- # Check if charge is paid
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
- # Check if charge is pending
58
+ # Is this charge still open?
39
59
  # @return [Boolean]
40
- def pending?
41
- status == 'pending'
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
- status == 'overdue'
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
- # @param params [Hash] optional payment details
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.post(self.class.show_url("settle", primary_key), params: params).call(class_name)
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
- class Company < Model
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