Class: Nfe::Resources::Companies
- Inherits:
-
AbstractResource
- Object
- AbstractResource
- Nfe::Resources::Companies
- 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.
Constant Summary collapse
- ENVELOPE =
"companies"
Instance Attribute Summary
Attributes inherited from AbstractResource
Instance Method Summary collapse
- #api_family ⇒ Symbol
- #build_certificate_status(details) ⇒ Nfe::CertificateStatus
- #certificate_status_or_nil(company_id) ⇒ Nfe::CertificateStatus?
-
#check_certificate_expiration(company_id, threshold_days: 30) ⇒ Hash?
Return an expiry warning when the certificate expires within
threshold_days, otherwisenil. -
#create(data) ⇒ Nfe::Company
Create a company.
- #expiring_or_nil(company_id, threshold_days) ⇒ Object
-
#find_by_name(name) ⇒ Array<Nfe::Company>
Find companies whose name contains
name(case-insensitive). -
#find_by_tax_number(tax_number) ⇒ Nfe::Company?
Find a company by federal tax number (CNPJ/CPF).
-
#get_certificate_status(company_id) ⇒ Nfe::CertificateStatus
Fetch certificate status, computing
days_until_expirationandexpiring_soonclient-side fromexpires_onwhen present. -
#get_companies_with_certificates ⇒ Array<Nfe::Company>
List companies that have a valid certificate.
-
#get_companies_with_expiring_certificates(threshold_days: 30) ⇒ Array<Nfe::Company>
List companies whose certificate expires within
threshold_days. - #json_body(data) ⇒ String
- #json_headers ⇒ Hash[String, String]
-
#list(page_index: 0, page_count: 100) ⇒ Nfe::ListResponse
List one page of companies.
-
#list_all ⇒ Array<Nfe::Company>
Fetch every company by auto-paginating with
page_count: 100until a short page is returned. -
#list_each ⇒ Enumerator<Nfe::Company>
Stream companies lazily, one per
yield, fetching pages on demand. - #normalize_tax_number(value) ⇒ String
-
#read_certificate_bytes(file) ⇒ String
Read certificate bytes from a file path or accept raw bytes verbatim.
-
#remove(company_id) ⇒ Hash
Delete a company.
-
#replace_certificate(company_id, file:, password:, filename: nil) ⇒ { uploaded: bool, message: String? }
Replace an existing certificate.
-
#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. -
#retrieve(company_id) ⇒ Nfe::Company
Retrieve a company by id.
-
#update(company_id, data) ⇒ Nfe::Company
Update a company.
-
#upload_certificate(company_id, file:, password:, filename: nil) ⇒ Hash
Upload a digital certificate, pre-validating it locally (fail-fast) before POSTing a
multipart/form-databody to/companies/{id}/certificatewith thefileandpasswordfields. -
#valid_email?(value) ⇒ Boolean
Structural e-mail check without a backtracking-prone regex (avoids the polynomial ReDoS in +[^\s@]+@[^\s@]+.[^\s@]++).
-
#validate_certificate(file:, password:) ⇒ Nfe::CertificateInfo
Validate a PKCS#12 certificate locally (no HTTP) and extract its metadata.
-
#validate_company_data(data) ⇒ void
Validate
federalTaxNumberformat/length and e-mail without coercing to Integer or running check-digit rules (design D12).
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_family ⇒ Symbol
26 27 28 |
# File 'lib/nfe/resources/companies.rb', line 26 def api_family :main end |
#build_certificate_status(details) ⇒ Nfe::CertificateStatus
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?
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.
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.
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
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.
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.
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.
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_certificates ⇒ Array<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).
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.
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
252 253 254 |
# File 'lib/nfe/resources/companies.rb', line 252 def json_body(data) JSON.generate(data) end |
#json_headers ⇒ 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.
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_all ⇒ Array<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.
66 67 68 |
# File 'lib/nfe/resources/companies.rb', line 66 def list_all list_each.to_a end |
#list_each ⇒ Enumerator<Nfe::Company>
Stream companies lazily, one per yield, fetching pages on demand.
Convenience helper; not optimized for large accounts.
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
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).
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).
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.
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.
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.
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.
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.
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.
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.
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.
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 |