Skip to main content

lib/sat/cfdi/descarga/masiva/paquete.ex

defmodule Sat.Cfdi.Descarga.Masiva.Paquete do
  @moduledoc """
  Servicio `DescargaMasivaSolicitudes` del WS de Descarga Masiva.

  Endpoint: `https://cfdidescargamasiva.clouda.sat.gob.mx/DescargaMasivaService.svc`.

  Descarga un paquete por su `id_paquete`. La respuesta contiene el contenido
  del ZIP en base64 dentro del sobre SOAP. Cada paquete puede contener hasta
  10,000 CFDIs.
  """

  alias Sat.Certificados.Credential
  alias Sat.Cfdi.Descarga.Masiva.Internal.{Http, Parser, SoapEnvelope}
  alias Sat.Cfdi.Descarga.Masiva.Types.{Paquete, Token}

  @endpoint "https://cfdidescargamasiva.clouda.sat.gob.mx/DescargaMasivaService.svc"
  @soap_action "http://DescargaMasivaTerceros.sat.gob.mx/IDescargaMasivaTercerosService/Descargar"

  @doc """
  Descarga un paquete y devuelve su contenido como bytes ZIP.

  Opciones:
    * `:credential` (requerido) — FIEL
    * `:rfc_solicitante` (requerido si la FIEL no es del solicitante)
    * `:endpoint` — override
    * `:timeout` — HTTP timeout (default 60000 ms para paquetes grandes)
  """
  @spec descargar(Token.t(), String.t(), keyword()) ::
          {:ok, Paquete.t()} | {:error, term()}
  def descargar(%Token{} = token, id_paquete, opts \\ []) when is_binary(id_paquete) do
    opts = Keyword.put_new(opts, :timeout, 60_000)

    with {:ok, %Credential{} = cred} <- fetch_credential(opts),
         {:ok, rfc} <- fetch_rfc(opts, cred),
         envelope = SoapEnvelope.build_descarga(cred, rfc, id_paquete, token.value),
         endpoint = Keyword.get(opts, :endpoint, @endpoint),
         http_opts = Keyword.put(opts, :token, token.value),
         {:ok, %{status: 200, body: body}} <-
           Http.post_soap(endpoint, @soap_action, envelope, http_opts),
         :ok <- Parser.detect_fault(body) do
      Parser.parse_descarga(body, id_paquete)
    else
      {:ok, %{status: status, body: body}} ->
        {:error, {:http_error, status, body}}

      {:error, _} = e ->
        e
    end
  end

  @doc "Endpoint del servicio."
  def endpoint, do: @endpoint

  @doc "SOAPAction."
  def soap_action, do: @soap_action

  defp fetch_credential(opts) do
    case Keyword.fetch(opts, :credential) do
      {:ok, %Credential{} = c} -> {:ok, c}
      {:ok, _} -> {:error, {:invalid_option, :credential, "expected Sat.Certificados.Credential"}}
      :error -> {:error, {:missing_option, :credential}}
    end
  end

  defp fetch_rfc(opts, cred) do
    case Keyword.get(opts, :rfc_solicitante) do
      nil -> {:ok, Credential.rfc(cred)}
      rfc when is_binary(rfc) -> {:ok, rfc}
      _ -> {:error, {:invalid_option, :rfc_solicitante}}
    end
  end
end