Class: Nfe::Resources::Companies

Inherits:
AbstractResource show all
Defined in:
lib/nfe/resources/companies.rb,
sig/nfe/resources/companies.rbs

Overview

Companies resource for the :main host family (+https://api.nfe.io/v1/...+). Exposes the company CRUD plus the digital certificate lifecycle (upload/replace/validate/status) and convenience finders. The NFE.io API wraps company responses in a {"companies" => <object|array>} envelope, transparently unwrapped here before hydrating Company.

Examples:

company = client.companies.create(name: "Acme", federalTaxNumber: "12345678000199")
client.companies.retrieve(company.id)

Constant Summary collapse

ENVELOPE =

Returns:

  • (String)
"companies"

Instance Attribute Summary

Attributes inherited from AbstractResource

#client

Instance Method Summary collapse

Methods inherited from AbstractResource

#api_version, #build_list_page, #build_multipart_body, #cursor_list_page, #delete, #dig_key, #download, #extract_invoice_id, #full_path, #get, #handle_async_response, #hydrate, #hydrate_list, #initialize, #multipart_part, #page_list_page, #parse_json, #post, #put, #unwrap, #upload_multipart

Constructor Details

This class inherits a constructor from Nfe::Resources::AbstractResource

Instance Method Details

#api_familySymbol

Returns:

  • (Symbol)


26
27
28
# File 'lib/nfe/resources/companies.rb', line 26

def api_family
  :main
end

#build_certificate_status(details) ⇒ Nfe::CertificateStatus

Parameters:

  • details (Hash[untyped, untyped])

Returns:



317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/nfe/resources/companies.rb', line 317

def build_certificate_status(details)
  has_certificate = details["hasCertificate"] || details["has_certificate"] || false
  expires_on = details["expiresOn"] || details["expires_on"]
  valid = details.key?("isValid") ? details["isValid"] : details["valid"]

  days = nil
  expiring = nil
  if has_certificate && expires_on
    not_after = Time.parse(expires_on.to_s)
    days = Nfe::CertificateValidator.days_until_expiration(not_after)
    expiring = Nfe::CertificateValidator.expiring_soon?(not_after)
  end

  Nfe::CertificateStatus.new(
    has_certificate: has_certificate, expires_on: expires_on, valid: valid,
    days_until_expiration: days, expiring_soon: expiring, details: details
  )
end

#certificate_status_or_nil(company_id) ⇒ Nfe::CertificateStatus?

Parameters:

  • company_id (String)

Returns:



336
337
338
339
340
# File 'lib/nfe/resources/companies.rb', line 336

def certificate_status_or_nil(company_id)
  get_certificate_status(company_id)
rescue Nfe::Error
  nil
end

#check_certificate_expiration(company_id, threshold_days: 30) ⇒ Hash?

Return an expiry warning when the certificate expires within threshold_days, otherwise nil.

Parameters:

  • company_id (String)
  • threshold_days (Integer) (defaults to: 30)
  • threshold_days: (Integer) (defaults to: 30)

Returns:

  • (Hash, nil)

    { expiring: true, days_remaining:, expires_on: }.



211
212
213
214
215
216
217
218
219
220
221
# File 'lib/nfe/resources/companies.rb', line 211

def check_certificate_expiration(company_id, threshold_days: 30)
  status = get_certificate_status(company_id)
  return nil unless status.has_certificate && status.expires_on

  days = status.days_until_expiration
  return nil if days.nil?

  return unless days >= 0 && days < threshold_days

  { expiring: true, days_remaining: days, expires_on: status.expires_on }
end

#create(data) ⇒ Nfe::Company

Create a company. Validates federalTaxNumber format (11/14 digits, never coerced to Integer) and e-mail format when present.

Parameters:

  • data (Hash)

    company attributes (camelCase keys per the API).

Returns:



37
38
39
40
41
# File 'lib/nfe/resources/companies.rb', line 37

def create(data)
  validate_company_data(data)
  response = post("/companies", body: json_body(data), headers: json_headers)
  hydrate(Nfe::Company, unwrap(parse_json(response.body), ENVELOPE))
end

#expiring_or_nil(company_id, threshold_days) ⇒ Object

Parameters:

  • company_id (String)
  • threshold_days (Integer)

Returns:

  • (Object)


342
343
344
345
346
# File 'lib/nfe/resources/companies.rb', line 342

def expiring_or_nil(company_id, threshold_days)
  check_certificate_expiration(company_id, threshold_days: threshold_days)
rescue Nfe::Error
  nil
end

#find_by_name(name) ⇒ Array<Nfe::Company>

Find companies whose name contains name (case-insensitive). Convenience helper built on #list_all; not optimized for large accounts.

Parameters:

  • name (String)

Returns:

Raises:



137
138
139
140
141
142
143
# File 'lib/nfe/resources/companies.rb', line 137

def find_by_name(name)
  term = name.to_s.strip
  raise Nfe::InvalidRequestError, "nome de busca (name) não pode ser vazio" if term.empty?

  term = term.downcase
  list_all.select { |company| company.name.to_s.downcase.include?(term) }
end

#find_by_tax_number(tax_number) ⇒ Nfe::Company?

Find a company by federal tax number (CNPJ/CPF). Convenience helper built on #list_all plus client-side filtering; not optimized for large accounts.

Parameters:

  • tax_number (String, Integer)

Returns:



127
128
129
130
# File 'lib/nfe/resources/companies.rb', line 127

def find_by_tax_number(tax_number)
  normalized = normalize_tax_number(tax_number)
  list_each.find { |company| normalize_tax_number(company.federal_tax_number) == normalized }
end

#get_certificate_status(company_id) ⇒ Nfe::CertificateStatus

Fetch certificate status, computing days_until_expiration and expiring_soon client-side from expires_on when present.

Parameters:

  • company_id (String)

Returns:



198
199
200
201
202
203
# File 'lib/nfe/resources/companies.rb', line 198

def get_certificate_status(company_id)
  id = Nfe::IdValidator.company_id(company_id)
  response = get("/companies/#{id}/certificate")
  details = parse_json(response.body) || {}
  build_certificate_status(details)
end

#get_companies_with_certificatesArray<Nfe::Company>

List companies that have a valid certificate. Convenience helper: #list_all + one get_certificate_status per company (companies whose status lookup fails are skipped).

Returns:



228
229
230
231
232
233
# File 'lib/nfe/resources/companies.rb', line 228

def get_companies_with_certificates
  list_all.select do |company|
    status = certificate_status_or_nil(company.id.to_s)
    status&.has_certificate && status.valid
  end
end

#get_companies_with_expiring_certificates(threshold_days: 30) ⇒ Array<Nfe::Company>

List companies whose certificate expires within threshold_days. Convenience helper; companies whose status lookup fails are skipped.

Parameters:

  • threshold_days (Integer) (defaults to: 30)
  • threshold_days: (Integer) (defaults to: 30)

Returns:



240
241
242
243
244
# File 'lib/nfe/resources/companies.rb', line 240

def get_companies_with_expiring_certificates(threshold_days: 30)
  list_all.select do |company|
    expiring_or_nil(company.id.to_s, threshold_days)
  end
end

#json_body(data) ⇒ String

Parameters:

  • data (Object)

Returns:

  • (String)


252
253
254
# File 'lib/nfe/resources/companies.rb', line 252

def json_body(data)
  JSON.generate(data)
end

#json_headersHash[String, String]

Returns:

  • (Hash[String, String])


248
249
250
# File 'lib/nfe/resources/companies.rb', line 248

def json_headers
  { "Content-Type" => "application/json" }
end

#list(page_index: 0, page_count: 100) ⇒ Nfe::ListResponse

List one page of companies. The public page_index is 0-based and is sent to the API as its 1-based pageIndex (which must be >= 1); the 1-based page in the response is converted back to 0-based.

Parameters:

  • page_index (Integer) (defaults to: 0)

    0-based page index.

  • page_count (Integer) (defaults to: 100)

    page size.

  • page_index: (Integer) (defaults to: 0)
  • page_count: (Integer) (defaults to: 100)

Returns:



50
51
52
53
54
55
56
57
58
59
# File 'lib/nfe/resources/companies.rb', line 50

def list(page_index: 0, page_count: 100)
  response = get("/companies", query: { pageIndex: page_index + 1, pageCount: page_count })
  payload = parse_json(response.body) || {}
  items = (unwrap(payload, ENVELOPE) || []).map { |item| hydrate(Nfe::Company, item) }
  resolved_index = resolve_page_index(payload, page_index)
  Nfe::ListResponse.new(
    data: items,
    page: Nfe::ListPage.from_page(page_index: resolved_index, page_count: page_count)
  )
end

#list_allArray<Nfe::Company>

Fetch every company by auto-paginating with page_count: 100 until a short page is returned. Convenience helper; not optimized for large accounts.

Returns:



66
67
68
# File 'lib/nfe/resources/companies.rb', line 66

def list_all
  list_each.to_a
end

#list_eachEnumerator<Nfe::Company>

Stream companies lazily, one per yield, fetching pages on demand. Convenience helper; not optimized for large accounts.

Returns:



74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/nfe/resources/companies.rb', line 74

def list_each
  Enumerator.new do |yielder|
    page_index = 0
    loop do
      page = list(page_index: page_index, page_count: 100)
      page.data.each { |company| yielder << company }
      break if page.data.length < 100

      page_index += 1
    end
  end
end

#normalize_tax_number(value) ⇒ String

Parameters:

  • value (Object)

Returns:

  • (String)


301
302
303
# File 'lib/nfe/resources/companies.rb', line 301

def normalize_tax_number(value)
  value.to_s.gsub(/[^0-9A-Za-z]/, "")
end

#read_certificate_bytes(file) ⇒ String

Read certificate bytes from a file path or accept raw bytes verbatim. Raw PKCS#12 (DER) bytes contain NUL bytes, which would make File.file? raise "path name contains null byte" — so a value with a NUL byte is always treated as content, and the path check runs only on NUL-free strings (which cannot trigger that error).

Parameters:

  • file (String)

Returns:

  • (String)


310
311
312
313
314
315
# File 'lib/nfe/resources/companies.rb', line 310

def read_certificate_bytes(file)
  bytes = file.to_s.dup.force_encoding(Encoding::ASCII_8BIT)
  return File.binread(file) if file.is_a?(String) && !bytes.include?("\x00".b) && File.file?(file)

  bytes
end

#remove(company_id) ⇒ Hash

Delete a company. Named remove to avoid clashing with delete semantics (parity with the Node/PHP SDKs).

Parameters:

  • company_id (String)

Returns:

  • (Hash)

    { deleted: true, id: company_id }.



115
116
117
118
119
# File 'lib/nfe/resources/companies.rb', line 115

def remove(company_id)
  id = Nfe::IdValidator.company_id(company_id)
  delete("/companies/#{id}")
  { deleted: true, id: id }
end

#replace_certificate(company_id, file:, password:, filename: nil) ⇒ { uploaded: bool, message: String? }

Replace an existing certificate. Alias of #upload_certificate — the API handles replacement.

Parameters:

  • company_id (String)
  • file: (String)
  • password: (String)
  • filename: (String, nil) (defaults to: nil)

Returns:

  • ({ uploaded: bool, message: String? })

See Also:



189
190
191
# File 'lib/nfe/resources/companies.rb', line 189

def replace_certificate(company_id, file:, password:, filename: nil)
  upload_certificate(company_id, file: file, password: password, filename: filename)
end

#resolve_page_index(payload, requested_index) ⇒ Integer

API uses 1-based page; convert to 0-based, falling back to the requested index when the API omits it.

Parameters:

  • payload (Hash[untyped, untyped])
  • requested_index (Integer)

Returns:

  • (Integer)


258
259
260
261
262
263
# File 'lib/nfe/resources/companies.rb', line 258

def resolve_page_index(payload, requested_index)
  api_page = payload["page"] || payload[:page]
  return requested_index if api_page.nil?

  api_page - 1
end

#retrieve(company_id) ⇒ Nfe::Company

Retrieve a company by id.

Parameters:

  • company_id (String)

Returns:

Raises:



92
93
94
95
96
# File 'lib/nfe/resources/companies.rb', line 92

def retrieve(company_id)
  id = Nfe::IdValidator.company_id(company_id)
  response = get("/companies/#{id}")
  hydrate(Nfe::Company, unwrap(parse_json(response.body), ENVELOPE))
end

#update(company_id, data) ⇒ Nfe::Company

Update a company.

Parameters:

  • company_id (String)
  • data (Hash)

Returns:



103
104
105
106
107
108
# File 'lib/nfe/resources/companies.rb', line 103

def update(company_id, data)
  id = Nfe::IdValidator.company_id(company_id)
  validate_company_data(data)
  response = put("/companies/#{id}", body: json_body(data), headers: json_headers)
  hydrate(Nfe::Company, unwrap(parse_json(response.body), ENVELOPE))
end

#upload_certificate(company_id, file:, password:, filename: nil) ⇒ Hash

Upload a digital certificate, pre-validating it locally (fail-fast) before POSTing a multipart/form-data body to /companies/{id}/certificate with the file and password fields.

Parameters:

  • company_id (String)
  • file (String)

    file path or raw bytes.

  • password (String)
  • filename (String, nil) (defaults to: nil)

    used for the extension check and part name.

  • file: (String)
  • password: (String)
  • filename: (String, nil) (defaults to: nil)

Returns:

  • (Hash)

    { uploaded: bool, message: String? }.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/nfe/resources/companies.rb', line 165

def upload_certificate(company_id, file:, password:, filename: nil)
  id = Nfe::IdValidator.company_id(company_id)
  if filename && !Nfe::CertificateValidator.supported_format?(filename)
    raise Nfe::InvalidRequestError,
          "formato de certificado não suportado: apenas .pfx e .p12 são aceitos"
  end

  bytes = read_certificate_bytes(file)
  Nfe::CertificateValidator.validate(bytes, password) # fail-fast, raises on invalid

  response = upload_multipart(
    "/companies/#{id}/certificate",
    "file" => { filename: filename || "certificate.pfx",
                content: bytes, content_type: "application/x-pkcs12" },
    "password" => password
  )
  payload = parse_json(response.body) || {}
  { uploaded: true, message: payload["message"] }
end

#valid_email?(value) ⇒ Boolean

Structural e-mail check without a backtracking-prone regex (avoids the polynomial ReDoS in +[^\s@]+@[^\s@]+.[^\s@]++). Equivalent semantics: exactly one "@", no whitespace, and a dotted domain with non-empty leading/trailing labels. Every step is linear in the input length.

Parameters:

  • value (String)

Returns:

  • (Boolean)


290
291
292
293
294
295
296
297
298
299
# File 'lib/nfe/resources/companies.rb', line 290

def valid_email?(value)
  return false if value.match?(/\s/)

  local, separator, domain = value.partition("@")
  return false if separator.empty? || local.empty? || domain.empty?
  return false if domain.include?("@")
  return false if domain.start_with?(".") || domain.end_with?(".")

  domain.include?(".")
end

#validate_certificate(file:, password:) ⇒ Nfe::CertificateInfo

Validate a PKCS#12 certificate locally (no HTTP) and extract its metadata. The password and bytes are handled in-memory only.

Parameters:

  • file (String)

    a file path or the raw .pfx/.p12 bytes.

  • password (String)
  • file: (String)
  • password: (String)

Returns:

Raises:



152
153
154
# File 'lib/nfe/resources/companies.rb', line 152

def validate_certificate(file:, password:)
  Nfe::CertificateValidator.validate(read_certificate_bytes(file), password)
end

#validate_company_data(data) ⇒ void

This method returns an undefined value.

Validate federalTaxNumber format/length and e-mail without coercing to Integer or running check-digit rules (design D12). Tolerant of both camelCase and snake_case keys.

Parameters:

  • data (Hash[untyped, untyped])

Raises:



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/nfe/resources/companies.rb', line 268

def validate_company_data(data)
  tax = data[:federalTaxNumber] || data["federalTaxNumber"] ||
        data[:federal_tax_number] || data["federal_tax_number"]
  unless tax.nil?
    digits = normalize_tax_number(tax)
    unless [11, 14].include?(digits.length)
      raise Nfe::InvalidRequestError,
            "federalTaxNumber deve conter 11 dígitos (CPF) ou 14 (CNPJ)"
    end
  end

  email = data[:email] || data["email"]
  return if email.nil?
  return if valid_email?(email.to_s)

  raise Nfe::InvalidRequestError, "e-mail (email) com formato inválido"
end