Skip to main content

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

defmodule Sat.Cfdi.Descarga.Masiva.Solicitud do
  @moduledoc """
  Servicio `SolicitaDescarga` del WS de Descarga Masiva (v1.5).

  Endpoint: `https://cfdidescargamasivasolicitud.clouda.sat.gob.mx/SolicitaDescargaService.svc`.

  Registra una solicitud de descarga por rango de fechas, RFC emisor/receptor,
  tipo de comprobante, estado, complemento, UUID, etc. Retorna un
  `IdSolicitud` que se usara despues para verificar el estado.

  En v1.5 la operacion `SolicitaDescarga` fue reemplazada por tres operaciones:
    * `SolicitaDescargaEmitidos`  — facturas emitidas por el RFC
    * `SolicitaDescargaRecibidos` — facturas recibidas por el RFC
    * `SolicitaDescargaFolio`     — un CFDI especifico por UUID (Folio)

  La funcion `solicitar/3` selecciona automaticamente la operacion correcta
  segun `params.tipo_solicitud` (:emitidos | :recibidos | :folio).
  """

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

  @endpoint "https://cfdidescargamasivasolicitud.clouda.sat.gob.mx/SolicitaDescargaService.svc"

  @soap_action_base "http://DescargaMasivaTerceros.sat.gob.mx/ISolicitaDescargaService/"

  @doc """
  Registra una solicitud y retorna el `IdSolicitud`.

  Requiere un token vigente (`Sat.Cfdi.Descarga.Masiva.Autenticacion.autenticar/1`)
  y la FIEL para firmar el sobre SOAP.

  Selecciona la operacion SOAP segun `params.tipo_solicitud`:
    * `:emitidos`  → `SolicitaDescargaEmitidos`
    * `:recibidos` → `SolicitaDescargaRecibidos`
    * `:folio`     → `SolicitaDescargaFolio`

  Opciones:
    * `:credential` (requerido) — FIEL para firmar
    * `:endpoint`   — override
    * `:timeout`    — HTTP timeout

  IMPORTANTE: El SAT permite maximo 2 solicitudes con los mismos parametros
  (mismo RFC + mismo rango de fechas). La tercera solicitud identica devuelve
  `cod_estatus = "5002"` de forma permanente para esa combinacion.
  """
  @spec solicitar(Token.t(), SolicitudParams.t(), keyword()) ::
          {:ok, SolicitudResult.t()} | {:error, term()}
  def solicitar(%Token{} = token, %SolicitudParams{} = params, opts \\ []) do
    operation = soap_operation(params.tipo_solicitud)
    soap_action = @soap_action_base <> operation

    with {:ok, %Credential{} = cred} <- fetch_credential(opts),
         envelope = SoapEnvelope.build_solicitud(cred, params, token.value, operation),
         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_solicitud(body)
    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 base (sin sufijo de operacion)."
  def soap_action_base, do: @soap_action_base

  @doc "Selecciona la operacion SOAP segun el tipo de solicitud."
  def soap_operation(:emitidos), do: "SolicitaDescargaEmitidos"
  def soap_operation(:recibidos), do: "SolicitaDescargaRecibidos"
  def soap_operation(:folio), do: "SolicitaDescargaFolio"
  def soap_operation(_), do: "SolicitaDescargaEmitidos"

  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
end