Skip to main content

lib/cnpja.ex

defmodule Cnpja do
  @moduledoc """
  Elixir SDK for the CNPJá API.

  All functions return `{:ok, struct}` on success or `{:error, %Cnpja.Error{}}` on failure.

  ## Configuration

  Via `config.exs`:

      config :cnpja_ex, api_key: System.get_env("CNPJA_API_KEY")

  Via option (multi-tenant):

      Cnpja.get_office("37335118000180", api_key: "other-key")

  ## Example

      case Cnpja.get_office("37335118000180") do
        {:ok, office}                       -> office.alias
        {:error, %Cnpja.Error{status: 404}} -> "not found"
        {:error, %Cnpja.Error{status: 429, required: r}} -> "insufficient credits: \#{r}"
        {:error, %Cnpja.Error{}}            -> "generic error"
      end
  """

  alias Cnpja.Client

  @sdk_keys [:api_key, :base_url]

  @camel_map %{
    simples_history: "simplesHistory",
    registrations_source: "registrationsSource",
    max_age: "maxAge",
    max_stale: "maxStale",
    history: "history",
    fov: "fov",
    pages: "pages",
    alias_in: "aliasIn",
    alias_nin: "aliasNin",
    company_name_in: "companyNameIn",
    company_name_nin: "companyNameNin",
    legal_nature_in: "legalNatureIn",
    legal_nature_nin: "legalNatureNin",
    equity_gte: "equityGte",
    equity_lte: "equityLte",
    size_in: "sizeIn",
    simples_optant: "simplesOptant",
    simei_optant: "simeiOptant",
    status_in: "statusIn",
    municipality_in: "municipalityIn",
    state_in: "stateIn",
    zip_in: "zipIn",
    main_activity_in: "mainActivityIn",
    side_activity_in: "sideActivityIn",
    has_phone: "hasPhone",
    has_email: "hasEmail",
    type_in: "typeIn",
    name_in: "nameIn",
    name_nin: "nameNin",
    tax_id_in: "taxIdIn",
    age_in: "ageIn",
    country_in: "countryIn"
  }

  @doc """
  Returns the credit balance for the account associated with the API key.
  """
  @spec get_credit(keyword()) :: {:ok, Cnpja.Credit.t()} | {:error, Cnpja.Error.t()}
  def get_credit(opts \\ []) do
    with {:ok, body} <- Client.get("/credit", [], sdk_opts(opts)) do
      {:ok, Cnpja.Credit.from_map(body)}
    end
  end

  @doc """
  Looks up a Brazilian postal code (CEP).

  ## Example

      {:ok, zip} = Cnpja.get_zip("01310100")
      zip.city  #=> "São Paulo"
  """
  @spec get_zip(String.t(), keyword()) :: {:ok, Cnpja.Zip.t()} | {:error, Cnpja.Error.t()}
  def get_zip(code, opts \\ []) do
    with {:ok, body} <- Client.get("/zip/#{code}", [], sdk_opts(opts)) do
      {:ok, Cnpja.Zip.from_map(body)}
    end
  end

  @doc """
  Looks up a company by the first 8 digits of the CNPJ (root).

  ## Options

  - `:simples` — include Simples Nacional data
  - `:simples_history` — include Simples Nacional history
  """
  @spec get_company(String.t(), keyword()) :: {:ok, Cnpja.Company.t()} | {:error, Cnpja.Error.t()}
  def get_company(company_id, opts \\ []) do
    with {:ok, body} <- Client.get("/company/#{company_id}", build_query(opts), sdk_opts(opts)) do
      {:ok, Cnpja.Company.from_map(body)}
    end
  end

  @doc """
  Looks up an establishment by its full 14-digit CNPJ.

  ## Options

  - `:simples` — include Simples Nacional data
  - `:simples_history` — include Simples Nacional history
  - `:registrations` — state registrations: `"ALL"`, `"NONE"`, or comma-separated state codes
  - `:registrations_source` — IE source: `"CCC"` (default) or `"RECEIPTS"`
  - `:suframa` — include SUFRAMA data
  - `:geocoding` — include geographic coordinates
  - `:links` — certificate links, comma-separated
  - `:strategy` — cache strategy: `"CACHE_IF_ERROR"` | `"NO_CACHE"` | `"CACHE"`
  - `:max_age` — maximum cache age in days
  - `:max_stale` — stale cache tolerance in days
  """
  @spec get_office(String.t(), keyword()) :: {:ok, Cnpja.Office.t()} | {:error, Cnpja.Error.t()}
  def get_office(tax_id, opts \\ []) do
    with {:ok, body} <- Client.get("/office/#{tax_id}", build_query(opts), sdk_opts(opts)) do
      {:ok, Cnpja.Office.from_map(body)}
    end
  end

  @doc """
  Returns the map image of the establishment location (PNG binary).

  ## Options

  - `:width` — image width in pixels
  - `:height` — image height in pixels
  - `:zoom` — zoom level
  - `:scale` — scale factor
  - `:type` — map type
  """
  @spec get_office_map(String.t(), keyword()) :: {:ok, binary()} | {:error, Cnpja.Error.t()}
  def get_office_map(tax_id, opts \\ []) do
    Client.get_binary("/office/#{tax_id}/map", build_query(opts), sdk_opts(opts))
  end

  @doc """
  Returns the Street View image of the establishment (JPEG binary).

  ## Options

  - `:width` — image width in pixels
  - `:height` — image height in pixels
  - `:fov` — field of view angle
  """
  @spec get_office_street_view(String.t(), keyword()) ::
          {:ok, binary()} | {:error, Cnpja.Error.t()}
  def get_office_street_view(tax_id, opts \\ []) do
    Client.get_binary("/office/#{tax_id}/street", build_query(opts), sdk_opts(opts))
  end

  @doc """
  Searches establishments with filters.

  ## Options

  - `:token` — pagination cursor (mutually exclusive with all filters)
  - `:limit` — results per page
  - `:alias_in` / `:alias_nin` — include/exclude trade name terms
  - `:company_name_in` / `:company_name_nin` — include/exclude company name terms
  - `:legal_nature_in` / `:legal_nature_nin` — legal nature codes
  - `:equity_gte` / `:equity_lte` — share capital range
  - `:size_in` — company size IDs (`1`=ME, `3`=EPP, `5`=Other)
  - `:simples_optant` — enrolled in Simples Nacional (`true`/`false`)
  - `:simei_optant` — enrolled as MEI (`true`/`false`)
  - `:head` — headquarters only
  - `:status_in` — status codes (2=Active, 3=Suspended, 4=Unfit, 8=Closed)
  - `:municipality_in` / `:state_in` / `:zip_in` — location filters
  - `:main_activity_in` / `:side_activity_in` — CNAE filters
  - `:has_phone` / `:has_email` — contact presence filters
  """
  @spec search_offices(keyword()) :: {:ok, Cnpja.OfficeSearch.t()} | {:error, Cnpja.Error.t()}
  def search_offices(opts \\ []) do
    with {:ok, body} <- Client.get("/office", build_query(opts), sdk_opts(opts)) do
      {:ok, Cnpja.OfficeSearch.from_map(body)}
    end
  end

  @doc """
  Looks up a person by their CNPJá ID.
  """
  @spec get_person(String.t(), keyword()) :: {:ok, Cnpja.Person.t()} | {:error, Cnpja.Error.t()}
  def get_person(person_id, opts \\ []) do
    with {:ok, body} <- Client.get("/person/#{person_id}", [], sdk_opts(opts)) do
      {:ok, Cnpja.Person.from_map(body)}
    end
  end

  @doc """
  Searches persons with filters.

  ## Options

  - `:token` — pagination cursor (mutually exclusive with all filters)
  - `:limit` — results per page
  - `:type_in` — person types: `"NATURAL"`, `"LEGAL"`, `"FOREIGN"`, `"UNKNOWN"` (comma-separated)
  - `:name_in` / `:name_nin` — include/exclude name terms
  - `:tax_id_in` — partial CPF digits (positions 4–9, comma-separated)
  - `:age_in` — age ranges, e.g. `"21-30,31-40"`
  - `:country_in` — M49 country codes (comma-separated)
  """
  @spec search_persons(keyword()) :: {:ok, Cnpja.PersonSearch.t()} | {:error, Cnpja.Error.t()}
  def search_persons(opts \\ []) do
    with {:ok, body} <- Client.get("/person", build_query(opts), sdk_opts(opts)) do
      {:ok, Cnpja.PersonSearch.from_map(body)}
    end
  end

  @doc """
  Queries establishment data directly from the Receita Federal.
  """
  @spec get_rfb(String.t(), keyword()) :: {:ok, Cnpja.Rfb.t()} | {:error, Cnpja.Error.t()}
  def get_rfb(tax_id, opts \\ []) do
    with {:ok, body} <- Client.get("/rfb/#{tax_id}", build_query(opts), sdk_opts(opts)) do
      {:ok, Cnpja.Rfb.from_map(body)}
    end
  end

  @doc """
  Returns the Comprovante de Inscrição e de Situação Cadastral as a PDF binary.

  ## Options

  - `:pages` — pages to include: `"REGISTRATION"`, `"MEMBERS"`, or both comma-separated
  """
  @spec get_rfb_certificate(String.t(), keyword()) :: {:ok, binary()} | {:error, Cnpja.Error.t()}
  def get_rfb_certificate(tax_id, opts \\ []) do
    Client.get_binary("/rfb/#{tax_id}/certificate", build_query(opts), sdk_opts(opts))
  end

  @doc """
  Queries Simples Nacional and MEI data for a company.
  """
  @spec get_simples(String.t(), keyword()) :: {:ok, Cnpja.Simples.t()} | {:error, Cnpja.Error.t()}
  def get_simples(tax_id, opts \\ []) do
    with {:ok, body} <- Client.get("/simples/#{tax_id}", build_query(opts), sdk_opts(opts)) do
      {:ok, Cnpja.Simples.from_map(body)}
    end
  end

  @doc """
  Returns the Simples Nacional enrollment declaration as a PDF binary.
  """
  @spec get_simples_certificate(String.t(), keyword()) ::
          {:ok, binary()} | {:error, Cnpja.Error.t()}
  def get_simples_certificate(tax_id, opts \\ []) do
    Client.get_binary("/simples/#{tax_id}/certificate", [], sdk_opts(opts))
  end

  @doc """
  Queries state registrations from the CCC for the given states.

  ## Parameters

  - `tax_id` — full 14-digit CNPJ
  - `states` — comma-separated state codes or `"ALL"`

  ## Example

      {:ok, ccc} = Cnpja.get_ccc("37335118000180", "SP,MG")
  """
  @spec get_ccc(String.t(), String.t(), keyword()) ::
          {:ok, Cnpja.Ccc.t()} | {:error, Cnpja.Error.t()}
  def get_ccc(tax_id, states, opts \\ []) do
    with {:ok, body} <- Client.get("/ccc/#{tax_id}/#{states}", build_query(opts), sdk_opts(opts)) do
      {:ok, Cnpja.Ccc.from_map(body)}
    end
  end

  @doc """
  Returns the CCC fiscal regularity certificate as a PDF binary.

  ## Options

  - `:state` — specific state code to filter the certificate
  """
  @spec get_ccc_certificate(String.t(), keyword()) :: {:ok, binary()} | {:error, Cnpja.Error.t()}
  def get_ccc_certificate(tax_id, opts \\ []) do
    Client.get_binary("/ccc/#{tax_id}/certificate", build_query(opts), sdk_opts(opts))
  end

  @doc """
  Queries SUFRAMA enrollment data for an establishment.
  """
  @spec get_suframa(String.t(), keyword()) :: {:ok, Cnpja.Suframa.t()} | {:error, Cnpja.Error.t()}
  def get_suframa(tax_id, opts \\ []) do
    with {:ok, body} <- Client.get("/suframa/#{tax_id}", build_query(opts), sdk_opts(opts)) do
      {:ok, Cnpja.Suframa.from_map(body)}
    end
  end

  @doc """
  Returns the SUFRAMA fiscal incentives certificate as a PDF binary.
  """
  @spec get_suframa_certificate(String.t(), keyword()) ::
          {:ok, binary()} | {:error, Cnpja.Error.t()}
  def get_suframa_certificate(tax_id, opts \\ []) do
    Client.get_binary("/suframa/#{tax_id}/certificate", [], sdk_opts(opts))
  end

  defp sdk_opts(opts), do: Keyword.take(opts, @sdk_keys)

  defp build_query(opts) do
    opts
    |> Keyword.drop(@sdk_keys)
    |> Enum.map(fn {k, v} -> {Map.get(@camel_map, k, to_string(k)), v} end)
  end
end