defmodule Sat.Csf do @moduledoc """ Extrae datos estructurados de una **Constancia de Situación Fiscal (CSF)** emitida por el SAT a partir de su PDF. ## Uso {:ok, %Sat.Csf.Document{} = csf} = Sat.Csf.from_file("priv/csf.pdf") csf.identificacion.rfc #=> "MACA961017759" csf.regimenes #=> [%Sat.Csf.Regimen{regimen: "...", codigo: "626", ...}, ...] Bajo el capó usa `Pdf.Reader.read/2` con `dictionary: :es` para reconstruir el texto correctamente. El parser de la respuesta (`Sat.Csf.Parser`) usa posiciones X de tokens para separar columnas en las tablas de obligaciones. ## Errores - `{:error, :not_a_csf}` — el PDF no contiene los marcadores de sección de un CSF (no se detectó "Datos de Identificación del Contribuyente"). - cualquier error reportado por `Pdf.Reader.open/2` o `Pdf.Reader.read/2`. """ alias Sat.Csf.{Document, Parser} @type from_result :: {:ok, Document.t()} | {:error, term()} @default_read_opts [dictionary: :es] @doc """ Lee y parsea un CSF desde una ruta de archivo. Las opciones se pasan a `Pdf.Reader.read/2`. Por defecto se incluye `dictionary: :es` para que el extractor separe palabras pegadas (puedes sobrescribirlo pasando `dictionary: nil` o tu propia `MapSet`). """ @spec from_file(Path.t(), keyword()) :: from_result() def from_file(path, opts \\ []) when is_binary(path) do open_and_parse(path, opts) end @doc """ Lee y parsea un CSF desde un binario `.pdf` ya cargado en memoria. """ @spec from_binary(binary(), keyword()) :: from_result() def from_binary(<<"%PDF-", _::binary>> = bin, opts \\ []) do open_and_parse(bin, opts) end @doc """ Parsea un `%Pdf.Reader.Result{}` ya extraído. Útil si ya estás trabajando con el reader y quieres evitar reabrir el PDF. """ @spec from_result(Pdf.Reader.Result.t()) :: from_result() def from_result(%Pdf.Reader.Result{} = result) do Parser.parse(result) end defp open_and_parse(path_or_bin, opts) do read_opts = Keyword.merge(@default_read_opts, opts) with {:ok, doc} <- Pdf.Reader.open(path_or_bin), {:ok, result, _doc} <- Pdf.Reader.read(doc, read_opts) do Parser.parse(result) end end @doc false def version, do: Application.spec(:sat_csf, :vsn) end